openapi: 3.0.3
info:
  title: Freedom Messenger API
  version: 0.2.0
  license:
    name: AGPL-3.0-only
    url: https://www.gnu.org/licenses/agpl-3.0.en.html
  description: |
    Route-complete public API description for Freedom Messenger.

    Browser clients normally authenticate with JWT sessions. Integrations can
    authenticate with scoped API tokens in the same bearer header. Narrow API
    token clients must still pass normal object authorization checks such as
    chat membership. Only API tokens with admin:* can use admin override
    behavior, and admin routes still require an admin user identity.
servers:
  - url: https://your-server
    description: Self-hosted Freedom Messenger instance
security:
  - bearerAuth: []
tags:
  - name: Auth
    description: Login, recovery, session, and current-user operations.
  - name: Users
    description: User listing, profile, tags, and presence operations.
  - name: Settings
    description: Current-user workspace settings.
  - name: Chats
    description: Chat creation, membership, and per-chat controls.
  - name: Messages
    description: Message send, read, edit, reaction, pin, and delivery status operations.
  - name: Search
    description: Chat and global message search operations.
  - name: TOTP
    description: Two-factor authentication setup and verification.
  - name: Files
    description: Authenticated file upload and download.
  - name: Admin
    description: Admin-only workspace management operations.
  - name: Integrations
    description: API tokens, bot accounts, incoming webhooks, and outgoing webhooks.
  - name: Tasks
    description: Chat task creation, updates, activities, and notifications.
  - name: Tags
    description: User tag administration and tag lookup.
  - name: Calls
    description: TURN credentials for calls.
  - name: Notifications
    description: Push subscription and notification-device settings.
  - name: Apps
    description: App distribution and update endpoints.
  - name: WebSocket
    description: WebSocket authentication and connection endpoint.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT or fm_live API token
  parameters:
    ChatID:
      name: chatID
      in: path
      required: true
      schema: { type: string }
    MessageID:
      name: messageID
      in: path
      required: true
      schema: { type: string }
    UserID:
      name: userID
      in: path
      required: true
      schema: { type: string }
    TaskID:
      name: taskID
      in: path
      required: true
      schema: { type: string }
    TagID:
      name: tagID
      in: path
      required: true
      schema: { type: string }
    BroadcastID:
      name: broadcastID
      in: path
      required: true
      schema: { type: string }
    InviteID:
      name: inviteID
      in: path
      required: true
      schema: { type: string }
    WebhookID:
      name: webhookID
      in: path
      required: true
      schema: { type: string }
    BotID:
      name: botID
      in: path
      required: true
      schema: { type: string }
    CallbackID:
      name: callbackID
      in: path
      required: true
      schema: { type: string }
    DeviceID:
      name: deviceID
      in: path
      required: true
      schema: { type: string }
  responses:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Missing, invalid, expired, or revoked authentication.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: Authenticated but not allowed by role, scope, 2FA, or object membership.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    TooManyRequests:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          schema: { type: string }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string }
    User:
      type: object
      properties:
        id: { type: string }
        username: { type: string, nullable: true }
        display_name: { type: string }
        avatar_path: { type: string, nullable: true }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        role: { type: string, enum: [admin, member, bot] }
        totp_enabled: { type: boolean }
        deleted_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    AuthResponse:
      type: object
      properties:
        token: { type: string }
        user: { $ref: "#/components/schemas/User" }
        requires_2fa: { type: boolean }
    Chat:
      type: object
      properties:
        id: { type: string }
        chat_type: { type: string, enum: [direct, group] }
        name: { type: string, nullable: true }
        avatar_path: { type: string, nullable: true }
        description: { type: string, nullable: true }
        created_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ChatResponse:
      allOf:
        - $ref: "#/components/schemas/Chat"
        - type: object
          properties:
            display_name: { type: string }
            unread_count: { type: integer }
            muted: { type: boolean }
            archived: { type: boolean }
            last_message:
              type: object
              allOf:
                - $ref: "#/components/schemas/Message"
              nullable: true
    Message:
      type: object
      properties:
        id: { type: string }
        chat_id: { type: string }
        sender_id: { type: string, nullable: true }
        content: { type: string }
        msg_type: { type: string, enum: [text, file, voice, system] }
        file_name: { type: string, nullable: true }
        file_path: { type: string, nullable: true }
        file_size: { type: integer, format: int64, nullable: true }
        reply_to_id: { type: string, nullable: true }
        client_id: { type: string, nullable: true }
        edited_at: { type: string, format: date-time, nullable: true }
        deleted: { type: boolean }
        created_at: { type: string, format: date-time }
    UserSettings:
      type: object
      additionalProperties: false
      properties:
        msg_font_size: { type: string, example: "14" }
        ui_font_size: { type: string, example: "14" }
        theme: { type: string, example: dark }
        language: { type: string, example: en }
        notif_sound: { type: string, enum: ["true", "false"] }
        notifications_enabled: { type: string, enum: ["true", "false"] }
        message_notifications_enabled: { type: string, enum: ["true", "false"] }
        task_notifications_enabled: { type: string, enum: ["true", "false"] }
        call_notifications_enabled: { type: string, enum: ["true", "false"] }
        notification_vibration: { type: string, enum: ["true", "false"] }
        notification_preview: { type: string, enum: [private, sender, full] }
        quiet_hours_enabled: { type: string, enum: ["true", "false"] }
        quiet_hours_start: { type: string, example: "22:00" }
        quiet_hours_end: { type: string, example: "08:00" }
        notification_timezone: { type: string, example: Europe/Amsterdam }
    AdminServerSettings:
      type: object
      additionalProperties: false
      properties:
        min_passphrase_length:
          type: string
          pattern: "^[0-9]+$"
          example: "12"
        developer_thanks_enabled:
          type: string
          enum: ["true", "false"]
          example: "true"
    DeveloperThanksState:
      type: object
      additionalProperties: false
      required: [enabled]
      properties:
        enabled: { type: boolean }
    MessageBatchStatusRequest:
      type: object
      required: [message_ids, status]
      additionalProperties: false
      properties:
        message_ids:
          type: array
          minItems: 1
          maxItems: 200
          items: { type: string }
        status: { type: string, enum: [delivered, read] }
      example:
        message_ids: ["msg_123", "msg_456"]
        status: read
    Invite:
      type: object
      properties:
        id: { type: string }
        name: { type: string, nullable: true }
        invite_link: { type: string }
        max_uses: { type: integer, nullable: true }
        use_count: { type: integer }
        expires_at: { type: string, format: date-time, nullable: true }
        revoked: { type: boolean }
        created_at: { type: string, format: date-time }
    ApiToken:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        user_id: { type: string }
        created_by: { type: string, nullable: true }
        token_prefix: { type: string }
        scopes:
          type: array
          items: { type: string }
        rate_limit_per_min: { type: integer }
        last_used_at: { type: string, format: date-time, nullable: true }
        revoked_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
    IncomingWebhook:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        chat_id: { type: string }
        created_by: { type: string, nullable: true }
        token_prefix: { type: string }
        last_used_at: { type: string, format: date-time, nullable: true }
        revoked_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        url: { type: string, description: Returned only on creation. }
    OutgoingWebhook:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string, enum: [message.created, bot.mentioned] }
        chat_ids:
          type: array
          items: { type: string }
        active: { type: boolean }
        created_by: { type: string, nullable: true }
        last_delivery_at: { type: string, format: date-time, nullable: true }
        revoked_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    WebhookDelivery:
      type: object
      properties:
        id: { type: string }
        webhook_id: { type: string }
        event: { type: string }
        attempt: { type: integer }
        status: { type: string, enum: [success, failed] }
        status_code: { type: integer, nullable: true }
        response_body: { type: string, nullable: true }
        error: { type: string, nullable: true }
        next_retry_at: { type: string, format: date-time, nullable: true }
        delivered_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
    BotCallback:
      type: object
      properties:
        id: { type: string }
        bot_user_id: { type: string }
        name: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string, enum: [bot.mentioned] }
        chat_ids:
          type: array
          items: { type: string }
        active: { type: boolean }
        created_by: { type: string, nullable: true }
        last_delivery_at: { type: string, format: date-time, nullable: true }
        revoked_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    BotCallbackDelivery:
      type: object
      properties:
        id: { type: string }
        callback_id: { type: string }
        event: { type: string, enum: [bot.mentioned] }
        attempt: { type: integer }
        status: { type: string, enum: [success, failed] }
        status_code: { type: integer, nullable: true }
        response_body: { type: string, nullable: true }
        error: { type: string, nullable: true }
        next_retry_at: { type: string, format: date-time, nullable: true }
        delivered_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
    Task:
      type: object
      required: [id, chat_id, title, description, priority, files, done, created_by, created_at, updated_at]
      properties:
        id: { type: string }
        chat_id: { type: string }
        title: { type: string }
        description: { type: string }
        priority: { type: string, enum: [low, medium, high] }
        assignee_id: { type: string, nullable: true }
        due_date: { type: string, format: date-time, nullable: true }
        files:
          type: string
          description: JSON-encoded array of authenticated file paths attached to the task.
          example: '["/api/files/task-brief.pdf"]'
        done: { type: boolean }
        created_by: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    TaskActivity:
      type: object
      required: [id, task_id, user_id, action, detail, created_at]
      properties:
        id: { type: string }
        task_id: { type: string }
        user_id: { type: string }
        action:
          type: string
          description: Activity type such as created, title_changed, priority_changed, assignee_changed, marked_done, or marked_undone.
        detail: { type: string }
        created_at: { type: string, format: date-time }
    TaskCreateRequest:
      type: object
      required: [title]
      additionalProperties: false
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        description:
          type: string
          maxLength: 5000
          default: ""
        priority:
          type: string
          enum: [low, medium, high]
          default: medium
        assignee_id:
          type: string
          nullable: true
          description: User ID to assign, or null to leave unassigned.
        due_date:
          type: string
          format: date-time
          nullable: true
      example:
        title: "Prepare launch checklist"
        description: "Confirm copy, assets, and owner sign-off."
        priority: high
        assignee_id: "user_123"
        due_date: "2026-05-10T09:00:00Z"
    TaskUpdateRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        description:
          type: string
          maxLength: 5000
        priority:
          type: string
          enum: [low, medium, high]
        assignee_id:
          type: string
          nullable: true
          description: User ID to assign. Send an empty string to clear the assignee.
        due_date:
          oneOf:
            - type: string
              format: date-time
            - type: string
              enum: [""]
          description: Due date. Send an empty string to clear the due date.
        files:
          type: string
          description: JSON-encoded array of authenticated file paths.
          example: '["/api/files/task-brief.pdf"]'
        done: { type: boolean }
      example:
        priority: high
        done: true
    TaskNotificationSettingsRequest:
      type: object
      required: [level]
      additionalProperties: false
      properties:
        level:
          type: string
          enum: [all, assigned, none]
          description: all sends all task notifications, assigned sends only assigned-task notifications, none disables task notifications for this chat.
      example:
        level: assigned
    TaskNotificationSettingsResponse:
      type: object
      required: [task_notifications]
      properties:
        task_notifications:
          type: string
          enum: [all, assigned, none]
    UserChatSettings:
      type: object
      properties:
        muted: { type: boolean }
        archived: { type: boolean }
        pinned_at: { type: string, format: date-time, nullable: true }
        message_notifications: { type: string, enum: [all, mentions, none] }
        task_notifications: { type: string, enum: [all, assigned, none] }
        call_notifications: { type: string, enum: [inherit, allow, mute] }
        notification_preview: { type: string, enum: [inherit, private, sender, full] }
    ChatNotificationSettingsRequest:
      type: object
      additionalProperties: false
      properties:
        message_notifications: { type: string, enum: [all, mentions, none] }
        task_notifications: { type: string, enum: [all, assigned, none] }
        call_notifications: { type: string, enum: [inherit, allow, mute] }
        notification_preview: { type: string, enum: [inherit, private, sender, full] }
      example:
        message_notifications: mentions
        task_notifications: assigned
        call_notifications: inherit
        notification_preview: sender
    Tag:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        color: { type: string }
    TagCreateRequest:
      type: object
      required: [name]
      additionalProperties: false
      properties:
        name: { type: string }
        color: { type: string }
    TagUpdateRequest:
      type: object
      additionalProperties: false
      properties:
        name: { type: string }
        color: { type: string }
    Broadcast:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        body: { type: string }
        severity: { type: string, enum: [info, warning, critical] }
        category: { type: string, enum: [general, maintenance, update] }
        status: { type: string }
        starts_at: { type: string, format: date-time, nullable: true }
        ends_at: { type: string, format: date-time, nullable: true }
        dismissible: { type: boolean }
        send_push: { type: boolean }
        revision: { type: integer }
        created_by: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        published_at: { type: string, format: date-time, nullable: true }
        archived_at: { type: string, format: date-time, nullable: true }
        push_sent_at: { type: string, format: date-time, nullable: true }
    BroadcastRequest:
      type: object
      required: [title, body]
      additionalProperties: false
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 160
        body:
          type: string
          minLength: 1
          maxLength: 4000
        severity:
          type: string
          enum: [info, warning, critical]
          default: info
        category:
          type: string
          enum: [general, maintenance, update]
          default: general
        status:
          type: string
          enum: [draft, active]
          default: draft
        starts_at: { type: string, format: date-time, nullable: true }
        ends_at: { type: string, format: date-time, nullable: true }
        dismissible: { type: boolean, default: true }
        send_push: { type: boolean, default: false }
      example:
        title: Planned maintenance
        body: Voice calls will restart during the maintenance window.
        severity: warning
        category: maintenance
        status: draft
        starts_at: "2026-05-10T01:00:00Z"
        ends_at: "2026-05-10T02:00:00Z"
        dismissible: true
        send_push: false
    CoverSiteConfig:
      type: object
      additionalProperties: false
      properties:
        theme: { type: string, enum: [blog, business, portfolio] }
        title: { type: string }
        description: { type: string }
        heading: { type: string }
        content: { type: string }
    NotificationSettings:
      type: object
      additionalProperties: false
      properties:
        notifications_enabled: { type: boolean }
        messages_enabled: { type: boolean }
        tasks_enabled: { type: boolean }
        calls_enabled: { type: boolean }
        sound_enabled: { type: boolean }
        vibration_enabled: { type: boolean }
        preview_mode: { type: string, enum: [private, sender, full] }
        quiet_hours_enabled: { type: boolean }
        quiet_hours_start:
          type: string
          pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$"
          example: "22:00"
        quiet_hours_end:
          type: string
          pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$"
          example: "08:00"
        timezone:
          type: string
          example: Europe/Amsterdam
    Device:
      type: object
      properties:
        id: { type: string }
        device_id: { type: string }
        label: { type: string }
        platform: { type: string, example: web }
        app_kind: { type: string, example: browser }
        permission_state: { type: string, enum: [default, granted, denied] }
        enabled: { type: boolean }
        capabilities:
          type: string
          description: JSON-encoded capability map stored by the server.
          example: '{"push":true,"actions":true}'
        user_agent: { type: string }
        last_seen_at: { type: string, format: date-time, nullable: true }
        last_success_at: { type: string, format: date-time, nullable: true }
        last_failure_at: { type: string, format: date-time, nullable: true }
        failure_count: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    PushSubscribeRequest:
      type: object
      required: [endpoint, key_p256dh, key_auth, device_id]
      additionalProperties: false
      properties:
        endpoint: { type: string, format: uri }
        key_p256dh: { type: string }
        key_auth: { type: string }
        device_id: { type: string }
        label: { type: string }
        platform: { type: string, example: web }
        app_kind: { type: string, example: browser }
        permission_state: { type: string, enum: [default, granted, denied] }
        capabilities:
          type: object
          additionalProperties: { type: boolean }
          example: { push: true, actions: true }
        user_agent: { type: string }
        timezone: { type: string, example: Europe/Amsterdam }
    PushUnsubscribeRequest:
      type: object
      required: [endpoint]
      additionalProperties: false
      properties:
        endpoint: { type: string, format: uri }
    NotificationDevicePatchRequest:
      type: object
      required: [enabled]
      additionalProperties: false
      properties:
        enabled: { type: boolean }
    NotificationDeviceStateRequest:
      type: object
      required: [device_id]
      additionalProperties: false
      properties:
        device_id: { type: string }
        label: { type: string }
        platform: { type: string, example: web }
        app_kind: { type: string, example: browser }
        permission_state: { type: string, enum: [default, granted, denied] }
        capabilities:
          type: object
          additionalProperties: { type: boolean }
          example: { push: true, actions: true }
        user_agent: { type: string }
        timezone: { type: string, example: Europe/Amsterdam }
    AppFile:
      type: object
      properties:
        name: { type: string }
        platform: { type: string }
        size: { type: integer }
    Health:
      type: object
      additionalProperties: true
paths:
  /api/auth/join:
    post:
      operationId: postApiAuthJoin
      tags: [Auth]
      summary: Join with an invite token
      security: []
      x-fm-auth: public
      x-fm-rate-limit: 10 per 15 minutes per IP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [invite_token, username, display_name, passphrase]
              properties:
                invite_token: { type: string }
                username: { type: string }
                display_name: { type: string }
                passphrase: { type: string, format: password }
      responses:
        "201":
          description: Joined.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/auth/login:
    post:
      operationId: postApiAuthLogin
      tags: [Auth]
      summary: Login with username and passphrase
      security: []
      x-fm-auth: public
      x-fm-rate-limit: 20 per 15 minutes per IP plus account lockout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, passphrase]
              properties:
                username: { type: string }
                passphrase: { type: string, format: password }
                totp_code: { type: string }
                recovery_code: { type: string }
      responses:
        "200":
          description: Logged in or returned a pre-2FA token.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/auth/logout:
    post:
      operationId: postApiAuthLogout
      tags: [Auth]
      summary: Logout the current JWT session
      x-fm-auth: user-session
      responses:
        "200": { description: Logged out. }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/auth/me:
    get:
      operationId: getApiAuthMe
      tags: [Auth]
      summary: Return the current user
      x-fm-auth: user-or-api-token
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Current user.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/User" }
    patch:
      operationId: patchApiAuthMe
      tags: [Auth]
      summary: Update current user profile
      x-fm-auth: user-or-api-token
      x-fm-scopes: ["users:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                display_name: { type: string }
                email: { type: string, nullable: true }
                phone: { type: string, nullable: true }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Updated. }
  /api/auth/avatar:
    post:
      operationId: postApiAuthAvatar
      tags: [Auth, Files]
      summary: Upload current user avatar
      x-fm-auth: user-or-api-token
      x-fm-scopes: ["users:write"]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Avatar uploaded. }
  /api/auth/reset-password:
    post:
      operationId: postApiAuthResetPassword
      tags: [Auth]
      summary: Complete password reset with a reset token
      security: []
      x-fm-auth: public
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, passphrase]
              properties:
                token: { type: string }
                passphrase: { type: string, format: password }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Password reset. }
  /api/auth/reset-password/{token}:
    get:
      operationId: getApiAuthResetPasswordToken
      tags: [Auth]
      summary: Validate a password reset token
      security: []
      x-fm-auth: public
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Reset token status. }
  /api/auth/request-recovery:
    post:
      operationId: postApiAuthRequestRecovery
      tags: [Auth]
      summary: Request admin-assisted password recovery
      security: []
      x-fm-auth: public
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username]
              properties:
                username: { type: string }
                contact: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Recovery requested. }
  /api/users:
    get:
      operationId: getApiUsers
      tags: [Users]
      summary: List active users
      x-fm-auth: user-or-api-token
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Users.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/User" }
  /api/settings:
    get:
      operationId: getApiSettings
      tags: [Settings]
      summary: Get current user settings
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Settings.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserSettings" }
    put:
      operationId: putApiSettings
      tags: [Settings]
      summary: Update current user settings
      x-fm-scopes: ["users:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UserSettings" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Settings updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserSettings" }
  /api/developer-thanks:
    get:
      operationId: getApiDeveloperThanks
      tags: [Settings]
      summary: Get developer thank-you tab visibility
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Developer thank-you visibility.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeveloperThanksState" }
  /api/broadcasts/active:
    get:
      operationId: getApiBroadcastsActive
      tags: [Admin]
      summary: List active workspace broadcasts
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Active broadcasts.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Broadcast" }
  /api/broadcasts/{broadcastID}/dismiss:
    post:
      operationId: postApiBroadcastsBroadcastIDDismiss
      tags: [Admin]
      summary: Dismiss a broadcast for the current user
      x-fm-scopes: ["users:write"]
      parameters:
        - $ref: "#/components/parameters/BroadcastID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Dismissed. }
  /api/chats:
    get:
      operationId: getApiChats
      tags: [Chats]
      summary: List chats visible to the current user
      x-fm-scopes: ["chats:read"]
      parameters:
        - name: archived
          in: query
          schema: { type: boolean, default: false }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Chats.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ChatResponse" }
    post:
      operationId: postApiChats
      tags: [Chats]
      summary: Create a direct or group chat
      description: Bot accounts cannot create chats.
      x-fm-scopes: ["chats:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [chat_type]
              properties:
                chat_type: { type: string, enum: [direct, group] }
                name: { type: string, nullable: true }
                user_id: { type: string, nullable: true }
                member_ids:
                  type: array
                  items: { type: string }
      responses:
        "201":
          description: Chat created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Chat" }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/chats/{chatID}:
    patch:
      operationId: patchApiChatsChatID
      tags: [Chats]
      summary: Update a group chat
      x-fm-scopes: ["chats:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                avatar_path: { type: string }
                description: { type: string }
                settings: { type: object, additionalProperties: { type: string } }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Updated. }
  /api/chats/{chatID}/messages:
    get:
      operationId: getApiChatsChatIDMessages
      tags: [Messages]
      summary: List messages in a chat
      x-fm-scopes: ["messages:read"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
        - name: before
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 30, maximum: 100 }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Messages.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Message" }
    post:
      operationId: postApiChatsChatIDMessages
      tags: [Messages]
      summary: Send a message
      x-fm-scopes: ["messages:write"]
      x-fm-rate-limit: JWT users 60 per minute; API tokens use per-token limit
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content: { type: string }
                msg_type: { type: string, enum: [text, file, voice] }
                file_name: { type: string }
                file_path: { type: string }
                file_size: { type: integer, format: int64 }
                reply_to_id: { type: string }
                client_id: { type: string }
      responses:
        "201":
          description: Message created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
  /api/chats/{chatID}/members:
    get:
      operationId: getApiChatsChatIDMembers
      tags: [Chats]
      summary: List chat members
      x-fm-scopes: ["chats:read"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Members.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/User" }
    post:
      operationId: postApiChatsChatIDMembers
      tags: [Chats]
      summary: Add a member to a group chat
      x-fm-scopes: ["chats:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_id]
              properties:
                user_id: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Member added. }
  /api/chats/{chatID}/members/{userID}:
    delete:
      operationId: deleteApiChatsChatIDMembersUserID
      tags: [Chats]
      summary: Remove a member from a group chat
      x-fm-scopes: ["chats:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Member removed. }
  /api/chats/{chatID}/leave:
    delete:
      operationId: deleteApiChatsChatIDLeave
      tags: [Chats]
      summary: Leave a group chat
      x-fm-scopes: ["chats:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Left chat. }
  /api/chats/{chatID}/mute:
    post:
      operationId: postApiChatsChatIDMute
      tags: [Chats]
      summary: Mute or unmute a chat for the current user
      x-fm-scopes: ["chats:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                muted: { type: boolean }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Mute state updated. }
  /api/chats/{chatID}/archive:
    post:
      operationId: postApiChatsChatIDArchive
      tags: [Chats]
      summary: Archive or unarchive a chat
      x-fm-scopes: ["chats:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                archived: { type: boolean }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Archive state updated. }
  /api/chats/{chatID}/pin-chat:
    post:
      operationId: postApiChatsChatIDPinChat
      tags: [Chats]
      summary: Pin or unpin a chat
      x-fm-scopes: ["chats:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                pinned: { type: boolean }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Pin state updated. }
  /api/chats/{chatID}/typing:
    post:
      operationId: postApiChatsChatIDTyping
      tags: [Chats]
      summary: Send a typing indicator
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Typing event accepted. }
  /api/chats/{chatID}/settings:
    get:
      operationId: getApiChatsChatIDSettings
      tags: [Chats, Notifications]
      summary: Get current user's chat settings
      x-fm-scopes: ["chats:read"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Chat settings.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserChatSettings" }
  /api/chats/{chatID}/notification-settings:
    put:
      operationId: putApiChatsChatIDNotificationSettings
      tags: [Chats, Notifications]
      summary: Update per-chat notification settings
      x-fm-scopes: ["chats:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ChatNotificationSettingsRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Settings updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserChatSettings" }
  /api/chats/{chatID}/read:
    post:
      operationId: postApiChatsChatIDRead
      tags: [Messages]
      summary: Mark a chat as read
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Marked read. }
  /api/chats/{chatID}/pin:
    post:
      operationId: postApiChatsChatIDPin
      tags: [Messages]
      summary: Pin a message in a chat
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message_id]
              properties:
                message_id: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Message pinned. }
  /api/chats/{chatID}/pin/{messageID}:
    delete:
      operationId: deleteApiChatsChatIDPinMessageID
      tags: [Messages]
      summary: Unpin a message
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
        - $ref: "#/components/parameters/MessageID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Message unpinned. }
  /api/chats/{chatID}/pinned:
    get:
      operationId: getApiChatsChatIDPinned
      tags: [Messages]
      summary: List pinned messages
      x-fm-scopes: ["messages:read"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Pinned messages.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Message" }
  /api/online:
    get:
      operationId: getApiOnline
      tags: [Users]
      summary: List online user IDs
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Online map. }
  /api/chats/{chatID}/search:
    get:
      operationId: getApiChatsChatIDSearch
      tags: [Search]
      summary: Search messages within one chat
      x-fm-scopes: ["messages:read"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
        - name: q
          in: query
          required: true
          schema: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Search results. }
  /api/search/messages:
    get:
      operationId: getApiSearchMessages
      tags: [Search]
      summary: Search messages across accessible chats
      x-fm-scopes: ["messages:read"]
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Search results. }
  /api/messages/batch-status:
    post:
      operationId: postApiMessagesBatchStatus
      tags: [Messages]
      summary: Update delivery/read status for multiple messages
      x-fm-scopes: ["messages:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MessageBatchStatusRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Status updated. }
  /api/messages/{messageID}/status:
    post:
      operationId: postApiMessagesMessageIDStatus
      tags: [Messages]
      summary: Update message status
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/MessageID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Status updated. }
  /api/messages/{messageID}:
    patch:
      operationId: patchApiMessagesMessageID
      tags: [Messages]
      summary: Edit a message
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/MessageID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Message edited. }
    delete:
      operationId: deleteApiMessagesMessageID
      tags: [Messages]
      summary: Delete a message
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/MessageID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Message deleted. }
  /api/messages/{messageID}/forward:
    post:
      operationId: postApiMessagesMessageIDForward
      tags: [Messages]
      summary: Forward a message to another chat
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/MessageID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to_chat_id]
              properties:
                to_chat_id: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201": { description: Message forwarded. }
  /api/messages/{messageID}/react:
    post:
      operationId: postApiMessagesMessageIDReact
      tags: [Messages]
      summary: Add or remove a reaction
      x-fm-scopes: ["messages:write"]
      parameters:
        - $ref: "#/components/parameters/MessageID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [emoji]
              properties:
                emoji: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Reaction toggled. }
  /api/messages/{messageID}/reactions:
    get:
      operationId: getApiMessagesMessageIDReactions
      tags: [Messages]
      summary: List reactions for a message
      x-fm-scopes: ["messages:read"]
      parameters:
        - $ref: "#/components/parameters/MessageID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Reactions. }
  /api/totp/status:
    get:
      operationId: getApiTotpStatus
      tags: [TOTP]
      summary: Get 2FA status
      x-fm-auth: user-session
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: 2FA status. }
  /api/totp/setup:
    post:
      operationId: postApiTotpSetup
      tags: [TOTP]
      summary: Start TOTP setup
      x-fm-auth: user-session
      responses:
        "200": { description: TOTP secret and recovery codes. }
        "403": { $ref: "#/components/responses/Forbidden" }
  /api/totp/enable:
    post:
      operationId: postApiTotpEnable
      tags: [TOTP]
      summary: Enable TOTP with a code
      x-fm-auth: user-session
      x-fm-rate-limit: 10 per 5 minutes per IP
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Enabled. }
  /api/totp/disable:
    post:
      operationId: postApiTotpDisable
      tags: [TOTP]
      summary: Disable TOTP with a code
      x-fm-auth: user-session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Disabled. }
  /api/totp/disable-simple:
    post:
      operationId: postApiTotpDisableSimple
      tags: [TOTP]
      summary: Disable TOTP when workspace enforcement is off
      x-fm-auth: user-session
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Disabled. }
  /api/totp/verify:
    post:
      operationId: postApiTotpVerify
      tags: [TOTP]
      summary: Verify TOTP during login
      x-fm-auth: user-session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                code: { type: string }
                recovery_code: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Verified and issued final token. }
  /api/files:
    post:
      operationId: postApiFiles
      tags: [Files]
      summary: Upload a file
      x-fm-scopes: ["files:write"]
      x-fm-rate-limit: JWT users 20 per hour; API tokens use per-token limit
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201": { description: File uploaded. }
  /api/files/{fileID}/{fileName}:
    get:
      operationId: getApiFilesFileIDFileName
      tags: [Files]
      summary: Download a file
      x-fm-scopes: ["files:read"]
      parameters:
        - name: fileID
          in: path
          required: true
          schema: { type: string }
        - name: fileName
          in: path
          required: true
          schema: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: File bytes. }
  /api/admin/users:
    get:
      operationId: getApiAdminUsers
      tags: [Admin, Users]
      summary: List users for admin dashboard
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Users. }
  /api/admin/users/deleted:
    get:
      operationId: getApiAdminUsersDeleted
      tags: [Admin, Users]
      summary: List soft-deleted users
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Deleted users. }
  /api/admin/users/{userID}:
    delete:
      operationId: deleteApiAdminUsersUserID
      tags: [Admin, Users]
      summary: Soft-delete a user
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: User deleted. }
  /api/admin/users/{userID}/soft-delete:
    post:
      operationId: postApiAdminUsersUserIDSoftDelete
      tags: [Admin, Users]
      summary: Soft-delete a user
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: User deleted. }
  /api/admin/users/{userID}/restore:
    post:
      operationId: postApiAdminUsersUserIDRestore
      tags: [Admin, Users]
      summary: Restore a soft-deleted user
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: User restored. }
  /api/admin/users/{userID}/purge:
    delete:
      operationId: deleteApiAdminUsersUserIDPurge
      tags: [Admin, Users]
      summary: Permanently purge a user
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: User purged. }
  /api/admin/users/{userID}/role:
    post:
      operationId: postApiAdminUsersUserIDRole
      tags: [Admin, Users]
      summary: Change a user role
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/UserID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role: { type: string, enum: [admin, member] }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Role changed. }
  /api/admin/users/{userID}/reset-2fa:
    post:
      operationId: postApiAdminUsersUserIDReset2fa
      tags: [Admin, Users, TOTP]
      summary: Reset a user's 2FA
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: 2FA reset. }
  /api/admin/users/{userID}/reset-password:
    post:
      operationId: postApiAdminUsersUserIDResetPassword
      tags: [Admin, Users, Auth]
      summary: Create password reset for a user
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Reset token created. }
  /api/admin/users/{userID}/dismiss-recovery:
    post:
      operationId: postApiAdminUsersUserIDDismissRecovery
      tags: [Admin, Users, Auth]
      summary: Dismiss a user's recovery request
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Recovery request dismissed. }
  /api/admin/invites:
    get:
      operationId: getApiAdminInvites
      tags: [Admin]
      summary: List invites
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Invites.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Invite" }
    post:
      operationId: postApiAdminInvites
      tags: [Admin]
      summary: Create an invite
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                max_uses: { type: integer }
                expires_at: { type: string, format: date-time }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201": { description: Invite created. }
  /api/admin/invites/{inviteID}:
    delete:
      operationId: deleteApiAdminInvitesInviteID
      tags: [Admin]
      summary: Revoke or delete an inactive invite
      description: Without purge, revokes the invite. With purge=true, permanently removes an already inactive invite from the admin list.
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/InviteID"
        - name: purge
          in: query
          schema: { type: boolean, default: false }
          description: Delete the invite instead of revoking it. Only inactive invites can be deleted.
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Invite revoked or deleted. }
  /api/admin/storage:
    get:
      operationId: getApiAdminStorage
      tags: [Admin]
      summary: Get storage usage
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Storage info. }
  /api/admin/stats:
    get:
      operationId: getApiAdminStats
      tags: [Admin]
      summary: Get admin stats
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Stats. }
  /api/admin/health:
    get:
      operationId: getApiAdminHealth
      tags: [Admin]
      summary: Get server health
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Health info.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Health" }
  /api/admin/logs:
    get:
      operationId: getApiAdminLogs
      tags: [Admin]
      summary: Query server logs
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - name: level
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer }
        - name: search
          in: query
          schema: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Log rows. }
  /api/admin/enforce-2fa:
    post:
      operationId: postApiAdminEnforce2fa
      tags: [Admin, TOTP]
      summary: Enable or disable workspace 2FA enforcement
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                required: { type: boolean }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Policy updated. }
  /api/admin/cover:
    get:
      operationId: getApiAdminCover
      tags: [Admin]
      summary: Get cover website configuration
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Cover config.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CoverSiteConfig" }
    put:
      operationId: putApiAdminCover
      tags: [Admin]
      summary: Update cover website configuration
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CoverSiteConfig" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Cover config updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CoverSiteConfig" }
  /api/admin/server-settings:
    get:
      operationId: getApiAdminServerSettings
      tags: [Admin]
      summary: Get admin server settings
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Server settings.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminServerSettings" }
    put:
      operationId: putApiAdminServerSettings
      tags: [Admin]
      summary: Update admin server settings
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AdminServerSettings" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Server settings updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminServerSettings" }
  /api/admin/broadcasts:
    get:
      operationId: getApiAdminBroadcasts
      tags: [Admin]
      summary: List broadcasts
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Broadcasts. }
    post:
      operationId: postApiAdminBroadcasts
      tags: [Admin]
      summary: Create a broadcast
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BroadcastRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201":
          description: Broadcast created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Broadcast" }
  /api/admin/broadcasts/{broadcastID}:
    patch:
      operationId: patchApiAdminBroadcastsBroadcastID
      tags: [Admin]
      summary: Update a broadcast
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/BroadcastID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BroadcastRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Broadcast updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Broadcast" }
    delete:
      operationId: deleteApiAdminBroadcastsBroadcastID
      tags: [Admin]
      summary: Delete a broadcast
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/BroadcastID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Broadcast deleted. }
  /api/admin/broadcasts/{broadcastID}/publish:
    post:
      operationId: postApiAdminBroadcastsBroadcastIDPublish
      tags: [Admin]
      summary: Publish a broadcast
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/BroadcastID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Broadcast published. }
  /api/admin/broadcasts/{broadcastID}/archive:
    post:
      operationId: postApiAdminBroadcastsBroadcastIDArchive
      tags: [Admin]
      summary: Archive a broadcast
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/BroadcastID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Broadcast archived. }
  /api/admin/api-tokens:
    get:
      operationId: getApiAdminApiTokens
      tags: [Integrations]
      summary: List API token metadata
      description: Full token secrets are never returned.
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: API token metadata.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ApiToken" }
    post:
      operationId: postApiAdminApiTokens
      tags: [Integrations]
      summary: Create an API token
      description: The full token is returned once and then stored only as a hash.
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scopes]
              properties:
                name: { type: string }
                environment: { type: string, enum: [live, test], default: live }
                scopes:
                  type: array
                  items: { type: string }
                rate_limit_per_min: { type: integer, default: 120, maximum: 1000 }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201": { description: API token with one-time token secret. }
  /api/admin/api-tokens/{tokenID}:
    delete:
      operationId: deleteApiAdminApiTokensTokenID
      tags: [Integrations]
      summary: Revoke or delete a revoked API token
      description: Without purge, revokes the token. With purge=true, permanently removes an already revoked token from the admin list.
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - name: tokenID
          in: path
          required: true
          schema: { type: string }
        - name: purge
          in: query
          schema: { type: boolean, default: false }
          description: Delete the token instead of revoking it. Only revoked tokens can be deleted.
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Token revoked or deleted. }
  /api/admin/bots:
    get:
      operationId: getApiAdminBots
      tags: [Integrations]
      summary: List bot accounts
      x-fm-auth: admin
      x-fm-scopes: ["bots:read", "admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Bots.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/User" }
    post:
      operationId: postApiAdminBots
      tags: [Integrations]
      summary: Create a bot account and first API token
      description: Bot tokens cannot use "admin:*" and bot accounts cannot create chats.
      x-fm-auth: admin
      x-fm-scopes: ["bots:write", "admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, description: Display name for backward-compatible clients. }
                display_name: { type: string, description: Optional display-name alias. }
                username: { type: string, description: Mention handle. Generated from name if omitted. }
                avatar_path: { type: string, nullable: true }
                email: { type: string, nullable: true }
                phone: { type: string, nullable: true }
                scopes:
                  type: array
                  items: { type: string }
                rate_limit_per_min: { type: integer, default: 300, maximum: 1000 }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "409": { description: Username unavailable. }
        "201": { description: Bot and one-time API token. }
  /api/admin/bots/{botID}:
    patch:
      operationId: patchApiAdminBotsBotID
      tags: [Integrations]
      summary: Update a bot profile
      description: Updates the bot identity shown in chats, message lists, and profile cards.
      x-fm-auth: admin
      x-fm-scopes: ["bots:write", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/BotID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                display_name: { type: string }
                name: { type: string, description: Backward-compatible display-name alias. }
                username: { type: string }
                avatar_path: { type: string, nullable: true }
                email: { type: string, nullable: true }
                phone: { type: string, nullable: true }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Username unavailable. }
        "200":
          description: Updated bot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/User" }
    delete:
      operationId: deleteApiAdminBotsBotID
      tags: [Integrations]
      summary: Delete a bot account
      description: Soft-deletes the bot, removes its chat memberships, revokes owned API tokens and bot-bound callbacks, and preserves historical messages.
      x-fm-auth: admin
      x-fm-scopes: ["bots:write", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/BotID"
      responses:
        "404": { $ref: "#/components/responses/NotFound" }
        "200":
          description: Bot soft-deleted.
          content:
            application/json:
              schema:
                type: object
                required: [status, bot_id]
                additionalProperties: false
                properties:
                  status: { type: string, enum: [soft_deleted] }
                  bot_id: { type: string }
  /api/admin/bots/{botID}/callbacks:
    get:
      operationId: getApiAdminBotsBotIDCallbacks
      tags: [Integrations]
      summary: List bot-bound callbacks
      description: Lists callbacks owned by one bot account. Secrets are never returned after creation.
      x-fm-auth: admin
      x-fm-scopes: ["bots:read", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/BotID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "200":
          description: Bot callbacks.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/BotCallback" }
    post:
      operationId: postApiAdminBotsBotIDCallbacks
      tags: [Integrations]
      summary: Create a bot-bound callback
      description: Creates a signed callback invoked when this bot is mentioned in a chat. HTTPS is required except localhost HTTP. Explicit chat filters require the bot to be a member of each chat; scoped API tokens must also belong to those chats.
      x-fm-auth: admin
      x-fm-scopes: ["bots:write", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/BotID"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url]
              properties:
                name: { type: string }
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string, enum: [bot.mentioned] }
                  default: [bot.mentioned]
                chat_ids:
                  type: array
                  items: { type: string }
                active: { type: boolean, default: true }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "201": { description: Bot callback with one-time signing_secret. }
  /api/admin/bot-callbacks/{callbackID}:
    delete:
      operationId: deleteApiAdminBotCallbacksCallbackID
      tags: [Integrations]
      summary: Revoke or delete an inactive bot callback
      description: Without purge, revokes the callback. With purge=true, permanently removes an inactive or revoked callback and its delivery logs from the admin list.
      x-fm-auth: admin
      x-fm-scopes: ["bots:write", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/CallbackID"
        - name: purge
          in: query
          schema: { type: boolean, default: false }
          description: Delete the bot callback instead of revoking it. Only disabled or revoked callbacks can be deleted.
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Bot callback revoked or deleted. }
  /api/admin/bot-callbacks/{callbackID}/deliveries:
    get:
      operationId: getApiAdminBotCallbacksCallbackIDDeliveries
      tags: [Integrations]
      summary: List bot callback delivery attempts
      x-fm-auth: admin
      x-fm-scopes: ["bots:read", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/CallbackID"
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 100 }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Bot callback delivery attempts.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/BotCallbackDelivery" }
  /api/admin/incoming-webhooks:
    get:
      operationId: getApiAdminIncomingWebhooks
      tags: [Integrations]
      summary: List incoming webhooks
      x-fm-auth: admin
      x-fm-scopes: ["webhooks:read", "admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Incoming webhooks.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/IncomingWebhook" }
    post:
      operationId: postApiAdminIncomingWebhooks
      tags: [Integrations]
      summary: Create an incoming webhook
      description: Scoped webhooks:write API tokens can only create incoming webhooks for chats the token owner belongs to. API tokens need admin:* for admin override behavior.
      x-fm-auth: admin
      x-fm-scopes: ["webhooks:write", "admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, chat_id]
              properties:
                name: { type: string }
                chat_id: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201": { description: Incoming webhook with one-time URL. }
  /api/admin/incoming-webhooks/{webhookID}:
    delete:
      operationId: deleteApiAdminIncomingWebhooksWebhookID
      tags: [Integrations]
      summary: Revoke or delete a revoked incoming webhook
      description: Without purge, revokes the webhook URL. With purge=true, permanently removes an already revoked webhook from the admin list.
      x-fm-auth: admin
      x-fm-scopes: ["webhooks:write", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/WebhookID"
        - name: purge
          in: query
          schema: { type: boolean, default: false }
          description: Delete the incoming webhook instead of revoking it. Only revoked incoming webhooks can be deleted.
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Incoming webhook revoked or deleted. }
  /api/webhooks/{webhookToken}:
    post:
      operationId: postApiWebhooksWebhookToken
      tags: [Integrations]
      summary: Post a message through an incoming webhook
      security: []
      x-fm-auth: incoming-webhook-url-secret
      x-fm-rate-limit: 120 per minute per IP
      parameters:
        - name: webhookToken
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
                username: { type: string }
                icon: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201":
          description: Message created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Message" }
  /api/admin/outgoing-webhooks:
    get:
      operationId: getApiAdminOutgoingWebhooks
      tags: [Integrations]
      summary: List outgoing webhooks
      x-fm-auth: admin
      x-fm-scopes: ["webhooks:read", "admin:*"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Outgoing webhooks.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/OutgoingWebhook" }
    post:
      operationId: postApiAdminOutgoingWebhooks
      tags: [Integrations]
      summary: Create an outgoing webhook
      description: Only HTTPS URLs are accepted except localhost HTTP for development. Scoped webhooks:write API tokens must provide chat_ids and can only subscribe to chats the token owner belongs to. Global outgoing webhooks require admin:*.
      x-fm-auth: admin
      x-fm-scopes: ["webhooks:write", "admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, url]
              properties:
                name: { type: string }
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string, enum: [message.created, bot.mentioned] }
                  default: [message.created]
                chat_ids:
                  type: array
                  items: { type: string }
                active: { type: boolean, default: true }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201": { description: Outgoing webhook with one-time signing_secret. }
  /api/admin/outgoing-webhooks/{webhookID}:
    delete:
      operationId: deleteApiAdminOutgoingWebhooksWebhookID
      tags: [Integrations]
      summary: Revoke or delete an inactive outgoing webhook
      description: Without purge, revokes the webhook. With purge=true, permanently removes an inactive or revoked webhook and its delivery logs from the admin list.
      x-fm-auth: admin
      x-fm-scopes: ["webhooks:write", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/WebhookID"
        - name: purge
          in: query
          schema: { type: boolean, default: false }
          description: Delete the outgoing webhook instead of revoking it. Only disabled or revoked outgoing webhooks can be deleted.
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Outgoing webhook revoked or deleted. }
  /api/admin/outgoing-webhooks/{webhookID}/deliveries:
    get:
      operationId: getApiAdminOutgoingWebhooksWebhookIDDeliveries
      tags: [Integrations]
      summary: List outgoing webhook delivery attempts
      x-fm-auth: admin
      x-fm-scopes: ["webhooks:read", "admin:*"]
      parameters:
        - $ref: "#/components/parameters/WebhookID"
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 100 }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Delivery attempts.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/WebhookDelivery" }
  /api/chats/{chatID}/tasks:
    get:
      operationId: getApiChatsChatIDTasks
      tags: [Tasks]
      summary: List tasks in a chat
      x-fm-scopes: ["tasks:read"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Tasks.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Task" }
    post:
      operationId: postApiChatsChatIDTasks
      tags: [Tasks]
      summary: Create a task in a chat
      x-fm-scopes: ["tasks:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TaskCreateRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201":
          description: Task created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Task" }
  /api/tasks/{taskID}:
    patch:
      operationId: patchApiTasksTaskID
      tags: [Tasks]
      summary: Update a task
      x-fm-scopes: ["tasks:write"]
      parameters:
        - $ref: "#/components/parameters/TaskID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TaskUpdateRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Task updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Task" }
    delete:
      operationId: deleteApiTasksTaskID
      tags: [Tasks]
      summary: Delete a task
      x-fm-scopes: ["tasks:write"]
      parameters:
        - $ref: "#/components/parameters/TaskID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Task deleted. }
  /api/tasks/{taskID}/activities:
    get:
      operationId: getApiTasksTaskIDActivities
      tags: [Tasks]
      summary: List task activity
      x-fm-scopes: ["tasks:read"]
      parameters:
        - $ref: "#/components/parameters/TaskID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Task activities.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/TaskActivity" }
  /api/chats/{chatID}/task-notifications:
    post:
      operationId: postApiChatsChatIDTaskNotifications
      tags: [Tasks, Notifications]
      summary: Update task notification settings for a chat
      x-fm-scopes: ["tasks:write"]
      parameters:
        - $ref: "#/components/parameters/ChatID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TaskNotificationSettingsRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Settings updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TaskNotificationSettingsResponse" }
  /api/tags:
    get:
      operationId: getApiTags
      tags: [Tags]
      summary: List user tags
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Tags.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Tag" }
  /api/users/tags:
    get:
      operationId: getApiUsersTags
      tags: [Tags, Users]
      summary: List tag assignments by user
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: User tag assignments. }
  /api/admin/tags:
    post:
      operationId: postApiAdminTags
      tags: [Admin, Tags]
      summary: Create a tag
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TagCreateRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201": { description: Tag created. }
  /api/admin/tags/{tagID}:
    put:
      operationId: putApiAdminTagsTagID
      tags: [Admin, Tags]
      summary: Update a tag
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/TagID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TagUpdateRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Tag updated. }
    delete:
      operationId: deleteApiAdminTagsTagID
      tags: [Admin, Tags]
      summary: Delete a tag
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/TagID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Tag deleted. }
  /api/admin/tags/{tagID}/users/{userID}:
    post:
      operationId: postApiAdminTagsTagIDUsersUserID
      tags: [Admin, Tags]
      summary: Assign a tag to a user
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/TagID"
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Tag assigned. }
    delete:
      operationId: deleteApiAdminTagsTagIDUsersUserID
      tags: [Admin, Tags]
      summary: Remove a tag from a user
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/TagID"
        - $ref: "#/components/parameters/UserID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Tag removed. }
  /api/admin/tags/{tagID}/members:
    get:
      operationId: getApiAdminTagsTagIDMembers
      tags: [Admin, Tags]
      summary: List users assigned to a tag
      x-fm-auth: admin
      x-fm-scopes: ["admin:*"]
      parameters:
        - $ref: "#/components/parameters/TagID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: User IDs. }
  /api/turn/credentials:
    get:
      operationId: getApiTurnCredentials
      tags: [Calls]
      summary: Issue temporary TURN credentials
      x-fm-scopes: ["users:read"]
      x-fm-rate-limit: 5 per hour per JWT user; API tokens use per-token limit
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: TURN credentials. }
  /api/push/vapid-key:
    get:
      operationId: getApiPushVapidKey
      tags: [Notifications]
      summary: Get Web Push VAPID public key
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: VAPID key. }
  /api/push/subscribe:
    post:
      operationId: postApiPushSubscribe
      tags: [Notifications]
      summary: Subscribe current device to push notifications
      x-fm-scopes: ["users:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PushSubscribeRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "201": { description: Subscription saved. }
  /api/push/unsubscribe:
    post:
      operationId: postApiPushUnsubscribe
      tags: [Notifications]
      summary: Unsubscribe from push notifications
      x-fm-scopes: ["users:write"]
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PushUnsubscribeRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Subscription removed. }
  /api/notifications/settings:
    get:
      operationId: getApiNotificationsSettings
      tags: [Notifications]
      summary: Get notification settings
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Notification settings.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/NotificationSettings" }
    put:
      operationId: putApiNotificationsSettings
      tags: [Notifications]
      summary: Update notification settings
      x-fm-scopes: ["users:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NotificationSettings" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Settings updated. }
  /api/notifications/devices:
    get:
      operationId: getApiNotificationsDevices
      tags: [Notifications]
      summary: List notification devices
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: Notification devices.
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Device" }
  /api/notifications/devices/{deviceID}:
    patch:
      operationId: patchApiNotificationsDevicesDeviceID
      tags: [Notifications]
      summary: Update a notification device
      x-fm-scopes: ["users:write"]
      parameters:
        - $ref: "#/components/parameters/DeviceID"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NotificationDevicePatchRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Device updated. }
    delete:
      operationId: deleteApiNotificationsDevicesDeviceID
      tags: [Notifications]
      summary: Delete a notification device
      x-fm-scopes: ["users:write"]
      parameters:
        - $ref: "#/components/parameters/DeviceID"
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: Device deleted. }
  /api/notifications/devices/current/state:
    post:
      operationId: postApiNotificationsDevicesCurrentState
      tags: [Notifications]
      summary: Update current notification device state
      x-fm-scopes: ["users:write"]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NotificationDeviceStateRequest" }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: State updated. }
  /api/apps:
    get:
      operationId: getApiApps
      tags: [Apps]
      summary: List downloadable app builds
      x-fm-scopes: ["users:read"]
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200":
          description: App files.
          content:
            application/json:
              schema:
                type: object
                properties:
                  files:
                    type: array
                    items: { $ref: "#/components/schemas/AppFile" }
  /api/apps/{filename}:
    get:
      operationId: getApiAppsFilename
      tags: [Apps]
      summary: Download an app build
      x-fm-scopes: ["users:read"]
      parameters:
        - name: filename
          in: path
          required: true
          schema: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "200": { description: File bytes. }
  /api/ws:
    get:
      operationId: getApiWs
      tags: [WebSocket]
      summary: WebSocket event stream
      description: Requires a JWT or API token with "messages:read". The same bearer token can be sent as the token query parameter for browser WebSocket clients.
      x-fm-scopes: ["messages:read"]
      parameters:
        - name: token
          in: query
          schema: { type: string }
      responses:
        "400": { $ref: "#/components/responses/BadRequest" }
        "101": { description: Switching protocols. }
