openapi: 3.1.0
info:
  title: Pollenate Cloud API
  description: |
    Feedback collection and analytics platform API.

    ## Authentication

    Most endpoints require authentication via JWT Bearer token or API key.

    - **JWT Token**: Obtained via OTP email verification, passed in `Authorization: Bearer <token>` header
    - **API Key**: For widget/external integrations, passed in `X-Pollenate-Key: <key>` header

    ## Organization Hierarchy

    - **Organizations**: Top-level accounts (companies)
    - **Brands**: Sub-organizations within an org (e.g., product lines)
    - **Inboxes**: Feedback collection endpoints within organizations (optionally filtered by brand)
    - **API Keys**: Can be org-wide or brand-scoped

  version: 1.1.0
  contact:
    name: Pollenate Support
    email: support@pollenate.dev
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: http://localhost:8787
    description: Local development
  - url: https://api.pollenate.dev
    description: Production

tags:
  - name: Auth
    description: Authentication and session management
  - name: Organizations
    description: Organization management
  - name: Brands
    description: Brand (sub-organization) management
  - name: Members
    description: Team member management
  - name: Invitations
    description: Team invitations (email-based)
  - name: Invite Links
    description: Shareable invite links for joining organizations
  - name: API Keys
    description: API key management
  - name: Inboxes
    description: Feedback inbox management
  - name: Feedback
    description: Feedback collection and retrieval
  - name: Analytics
    description: Impression tracking and analytics
  - name: Search
    description: Semantic search
  - name: Feedback Pages
    description: Feedback page form builder and public submissions
  - name: Themes
    description: Widget theme management

paths:
  # ==================== AUTH ====================
  /auth/otp/send:
    post:
      tags: [Auth]
      summary: Send OTP code
      description: Sends a 6-digit OTP code to the provided email address
      operationId: sendOtp
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  example: user@example.com
      responses:
        "200":
          description: OTP sent successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: OTP sent to user@example.com
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /auth/otp/verify:
    post:
      tags: [Auth]
      summary: Verify OTP code
      description: Verifies OTP code and returns JWT tokens
      operationId: verifyOtp
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code]
              properties:
                email:
                  type: string
                  format: email
                code:
                  type: string
                  pattern: '^\d{6}$'
                  example: "123456"
      responses:
        "200":
          description: Authentication successful
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthTokens"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          description: Invalid or expired OTP
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /auth/refresh:
    post:
      tags: [Auth]
      summary: Refresh access token
      description: Exchange refresh token for new access token
      operationId: refreshToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refreshToken]
              properties:
                refreshToken:
                  type: string
      responses:
        "200":
          description: Token refreshed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthTokens"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /auth/logout:
    post:
      tags: [Auth]
      summary: Logout
      description: Revoke current session
      operationId: logout
      responses:
        "204":
          description: Logged out successfully

  /auth/me:
    get:
      tags: [Auth]
      summary: Get current user
      description: Returns the authenticated user's profile and organizations
      operationId: getCurrentUser
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserWithOrganizations"
        "401":
          $ref: "#/components/responses/Unauthorized"
    patch:
      tags: [Auth]
      summary: Update current user profile
      description: Updates the authenticated user's profile (display name, avatar)
      operationId: updateProfile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
                  description: Display name
                avatarUrl:
                  type:
                    - string
                    - "null"
                  format: uri
                  description: Avatar image URL
      responses:
        "200":
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ==================== ORGANIZATIONS ====================
  /organizations:
    get:
      tags: [Organizations]
      summary: List organizations
      description: Returns organizations the user is a member of
      operationId: listOrganizations
      responses:
        "200":
          description: List of organizations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/OrganizationWithRole"
    post:
      tags: [Organizations]
      summary: Create organization
      description: Create a new organization (user becomes owner)
      operationId: createOrganization
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrganization"
      responses:
        "201":
          description: Organization created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Organization"
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          description: Organization slug already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /organizations/{orgId}:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    get:
      tags: [Organizations]
      summary: Get organization
      operationId: getOrganization
      responses:
        "200":
          description: Organization details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizationDetail"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [Organizations]
      summary: Update organization
      operationId: updateOrganization
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateOrganization"
      responses:
        "200":
          description: Organization updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Organization"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      tags: [Organizations]
      summary: Delete organization
      description: Permanently delete organization and all data (owner only)
      operationId: deleteOrganization
      responses:
        "204":
          description: Organization deleted
        "403":
          $ref: "#/components/responses/Forbidden"

  # ==================== ORGANIZATION MEMBERS ====================
  /organizations/{orgId}/members:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    get:
      tags: [Members]
      summary: List organization members
      operationId: listOrgMembers
      responses:
        "200":
          description: List of members
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Member"
  /organizations/{orgId}/members/{userId}:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - $ref: "#/components/parameters/UserId"
    patch:
      tags: [Members]
      summary: Update member role
      operationId: updateOrgMember
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role:
                  $ref: "#/components/schemas/MemberRole"
      responses:
        "200":
          description: Member updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Member"
    delete:
      tags: [Members]
      summary: Remove member
      operationId: removeOrgMember
      responses:
        "204":
          description: Member removed

  # ==================== INVITATIONS ====================
  /organizations/{orgId}/invitations:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    get:
      tags: [Invitations]
      summary: List pending invitations
      operationId: listInvitations
      responses:
        "200":
          description: List of invitations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Invitation"
    post:
      tags: [Invitations]
      summary: Send invitation
      description: Send email invitation to join organization
      operationId: createInvitation
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInvitation"
      responses:
        "201":
          description: Invitation sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invitation"
        "409":
          description: User already a member or has pending invitation

  /invitations/{token}:
    parameters:
      - name: token
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Invitations]
      summary: Get invitation details
      description: Get invitation info for acceptance page (public)
      operationId: getInvitation
      security: []
      responses:
        "200":
          description: Invitation details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InvitationPublic"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags: [Invitations]
      summary: Accept invitation
      description: Accept invitation and join organization
      operationId: acceptInvitation
      responses:
        "200":
          description: Invitation accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Organization"
  /organizations/{orgId}/invitations/{invitationId}:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - name: invitationId
        in: path
        required: true
        schema:
          type: string
        description: Invitation ID
    delete:
      tags: [Invitations]
      summary: Revoke invitation
      description: Cancel a pending invitation (admin only)
      operationId: revokeInvitation
      responses:
        "204":
          description: Invitation revoked
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ==================== INVITE LINKS ====================
  /organizations/{orgId}/invite-links:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    get:
      tags: [Invite Links]
      summary: List invite links
      description: List all invite links for the organization. Requires `members.invite` permission.
      operationId: listInviteLinks
      responses:
        "200":
          description: List of invite links
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/InviteLink"
    post:
      tags: [Invite Links]
      summary: Create invite link
      description: |
        Create a shareable invite link for the organization. Unlike email invitations,
        anyone with the link can join (no specific email required).
        Requires `members.invite` permission.
      operationId: createInviteLink
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInviteLink"
      responses:
        "201":
          description: Invite link created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InviteLink"
        "400":
          $ref: "#/components/responses/BadRequest"

  /organizations/{orgId}/invite-links/{linkId}:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - name: linkId
        in: path
        required: true
        schema:
          type: string
        description: Invite link ID
    patch:
      tags: [Invite Links]
      summary: Update invite link
      description: Update invite link settings. Requires `members.invite` permission.
      operationId: updateInviteLink
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                isActive:
                  type: boolean
                  description: Enable or disable the link
                label:
                  type: string
                  maxLength: 100
                  description: Admin-facing description
                role:
                  type: string
                  enum: [admin, member, billing, viewer]
                  description: Role assigned to new members
                maxUses:
                  type:
                    - integer
                    - "null"
                  minimum: 1
                  description: Maximum number of uses (null to remove limit)
                expiresAt:
                  type:
                    - string
                    - "null"
                  format: date-time
                  description: Expiration timestamp (null to remove expiry)
                brandIds:
                  type: array
                  items:
                    type: string
                  description: Brand IDs to grant access to on join
      responses:
        "200":
          description: Updated invite link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InviteLink"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Invite Links]
      summary: Delete invite link
      description: Permanently delete an invite link. Requires `members.invite` permission.
      operationId: deleteInviteLink
      responses:
        "200":
          description: Link deleted
        "404":
          $ref: "#/components/responses/NotFound"

  /join/{code}:
    parameters:
      - name: code
        in: path
        required: true
        schema:
          type: string
        description: 8-character invite link code
    get:
      tags: [Invite Links]
      summary: Get invite link preview
      description: Public endpoint returning organization name, role, and link status. No authentication required.
      operationId: getInviteLinkPreview
      security: []
      responses:
        "200":
          description: Invite link details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InviteLinkPreview"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags: [Invite Links]
      summary: Join via invite link
      description: |
        Accept an invite link and join the organization. Requires authentication.
        Returns the organization info for navigation.
        Sends an email notification to the admin who created the link.
      operationId: acceptInviteLink
      responses:
        "200":
          description: Joined organization successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Organization"
        "403":
          description: Organization at member limit
        "409":
          description: User is already a member
        "410":
          description: Link expired, deactivated, or at max uses

  # ==================== API KEYS ====================
  /organizations/{orgId}/api-keys:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    get:
      tags: [API Keys]
      summary: List API keys
      operationId: listApiKeys
      parameters:
        - name: brandId
          in: query
          schema:
            type: string
          description: Filter by brand
      responses:
        "200":
          description: List of API keys (key values not included)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ApiKeyMeta"
    post:
      tags: [API Keys]
      summary: Create API key
      operationId: createApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiKey"
      responses:
        "201":
          description: API key created (includes full key - only returned once!)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyWithSecret"

  /organizations/{orgId}/api-keys/{keyId}:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - name: keyId
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [API Keys]
      summary: Get API key metadata
      operationId: getApiKey
      responses:
        "200":
          description: API key metadata
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyMeta"
    delete:
      tags: [API Keys]
      summary: Revoke API key
      operationId: revokeApiKey
      responses:
        "204":
          description: API key revoked

  # ==================== BRANDS ====================
  /organizations/{orgId}/brands:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    get:
      tags: [Brands]
      summary: List brands
      operationId: listBrands
      responses:
        "200":
          description: List of brands
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Brand"
    post:
      tags: [Brands]
      summary: Create brand
      operationId: createBrand
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBrand"
      responses:
        "201":
          description: Brand created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Brand"

  /organizations/{orgId}/brands/{brandId}:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - $ref: "#/components/parameters/BrandId"
    get:
      tags: [Brands]
      summary: Get brand
      operationId: getBrand
      responses:
        "200":
          description: Brand details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BrandDetail"
    patch:
      tags: [Brands]
      summary: Update brand
      operationId: updateBrand
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateBrand"
      responses:
        "200":
          description: Brand updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Brand"
    delete:
      tags: [Brands]
      summary: Delete brand
      operationId: deleteBrand
      responses:
        "204":
          description: Brand deleted

  # ==================== BRAND MEMBERS ====================
  /organizations/{orgId}/brands/{brandId}/members:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - $ref: "#/components/parameters/BrandId"
    get:
      tags: [Members]
      summary: List brand members
      operationId: listBrandMembers
      responses:
        "200":
          description: List of brand members
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Member"
    post:
      tags: [Members]
      summary: Add brand member
      description: Add org member to brand team
      operationId: addBrandMember
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId]
              properties:
                userId:
                  type: string
                role:
                  type: string
                  enum: [admin, member]
                  default: member
      responses:
        "201":
          description: Member added to brand
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Member"

  # ==================== INBOXES ====================
  /organizations/{orgId}/inboxes:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    get:
      tags: [Inboxes]
      summary: List inboxes
      description: List inboxes for the organization, optionally filtered by brand
      operationId: listInboxes
      parameters:
        - name: brandId
          in: query
          schema:
            type: string
          description: Filter inboxes by brand
      responses:
        "200":
          description: List of inboxes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Inbox"
    post:
      tags: [Inboxes]
      summary: Create inbox
      operationId: createInbox
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInbox"
      responses:
        "201":
          description: Inbox created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Inbox"

  /organizations/{orgId}/inboxes/{inboxId}:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - $ref: "#/components/parameters/InboxId"
    get:
      tags: [Inboxes]
      summary: Get inbox
      operationId: getInbox
      responses:
        "200":
          description: Inbox details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InboxDetail"
    patch:
      tags: [Inboxes]
      summary: Update inbox
      operationId: updateInbox
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateInbox"
      responses:
        "200":
          description: Inbox updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Inbox"
    delete:
      tags: [Inboxes]
      summary: Delete inbox
      operationId: deleteInbox
      responses:
        "204":
          description: Inbox deleted

  /organizations/{orgId}/inboxes/{inboxId}/regenerate-key:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - $ref: "#/components/parameters/InboxId"
    post:
      tags: [Inboxes]
      summary: Regenerate inbox key
      description: Generate a new unique key for the inbox, invalidating the old one
      operationId: regenerateInboxKey
      responses:
        "200":
          description: New key generated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Inbox"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ==================== FEEDBACK ====================
  /collect:
    post:
      tags: [Feedback]
      summary: Submit feedback
      description: |
        Public endpoint for widget feedback submission. Requires API key with "collect" scope.

        This is the primary endpoint used by the embeddable widget to submit feedback.
        The API key authenticates the request and determines which organization the feedback belongs to.
      operationId: collectFeedback
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FeedbackSubmission"
      responses:
        "201":
          description: Feedback submitted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedbackEvent"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          description: Invalid or missing API key
        "403":
          description: API key lacks required scope or inbox access
        "429":
          description: Rate limit exceeded for plan

  /organizations/{orgId}/feedback:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    get:
      tags: [Feedback]
      summary: List feedback across organization
      operationId: listOrgFeedback
      parameters:
        - name: brandId
          in: query
          schema:
            type: string
        - name: inboxId
          in: query
          schema:
            type: string
        - name: type
          in: query
          schema:
            $ref: "#/components/schemas/FeedbackType"
        - name: since
          in: query
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          schema:
            type: string
            format: date-time
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Paginated feedback list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedbackList"

  /organizations/{orgId}/feedback/{feedbackId}:
    parameters:
      - $ref: "#/components/parameters/OrgId"
      - name: feedbackId
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Feedback]
      summary: Get feedback event
      operationId: getFeedback
      responses:
        "200":
          description: Feedback event details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedbackEvent"
    delete:
      tags: [Feedback]
      summary: Delete feedback
      operationId: deleteFeedback
      responses:
        "204":
          description: Feedback deleted

  # ==================== SEMANTIC SEARCH ====================
  /organizations/{orgId}/search:
    parameters:
      - $ref: "#/components/parameters/OrgId"
    post:
      tags: [Search]
      summary: Semantic search
      description: Search feedback using natural language query
      operationId: semanticSearch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                  minLength: 3
                  maxLength: 500
                  example: "users frustrated with login"
                brandId:
                  type: string
                inboxId:
                  type: string
                limit:
                  type: integer
                  minimum: 1
                  maximum: 100
                  default: 20
                threshold:
                  type: number
                  minimum: 0
                  maximum: 1
                  default: 0.7
                  description: Minimum similarity score
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      $ref: "#/components/schemas/SearchResult"
                  query:
                    type: string
                  totalResults:
                    type: integer

  # ==================== THEMES ====================
  /themes/{themeId}:
    parameters:
      - name: themeId
        in: path
        required: true
        schema:
          type: string
        description: Theme ID
    get:
      tags: [Themes]
      summary: Get theme by ID
      description: Retrieve a public or system theme by its ID
      operationId: getTheme
      security: []
      responses:
        "200":
          description: Theme details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Theme"
        "404":
          $ref: "#/components/responses/NotFound"

  /themes/inbox/{inboxKey}:
    parameters:
      - name: inboxKey
        in: path
        required: true
        schema:
          type: string
        description: Inbox key
    get:
      tags: [Themes]
      summary: Get theme for inbox
      description: Public endpoint used by the widget to load the theme associated with an inbox
      operationId: getInboxTheme
      security: []
      responses:
        "200":
          description: Theme tokens for the inbox (returns default theme if none configured)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Theme"

  # ==================== IMPRESSIONS ====================
  /impressions:
    post:
      tags: [Analytics]
      summary: Track widget impression
      description: |
        Track when a widget loads on a page. This is called automatically by the widget
        on page load to track page views/impressions separate from feedback submissions.

        Impressions are used to calculate conversion rates (impressions → feedback).
      operationId: trackImpression
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImpressionSubmission"
      responses:
        "201":
          description: Impression tracked
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Impression ID (used to link feedback to this impression)
                  success:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          description: Invalid or missing API key
        "429":
          description: Rate limit exceeded for plan

  # ==================== ANALYTICS ====================
  /analytics/summary:
    get:
      tags: [Analytics]
      summary: Get organization-wide analytics summary
      description: Returns impression and feedback analytics across all inboxes the user has access to
      operationId: getAnalyticsSummary
      parameters:
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 30
          description: Number of days to include in the summary
      responses:
        "200":
          description: Analytics summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalyticsSummary"

  /analytics/inboxes/{inboxId}/impressions:
    parameters:
      - $ref: "#/components/parameters/InboxId"
    get:
      tags: [Analytics]
      summary: Get inbox impression analytics
      description: Returns detailed impression analytics for a specific inbox
      operationId: getInboxImpressionAnalytics
      parameters:
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 30
        - name: groupBy
          in: query
          schema:
            type: string
            enum: [hour, day, week]
            default: day
          description: Time series grouping
      responses:
        "200":
          description: Inbox impression analytics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InboxImpressionAnalytics"

  /analytics/inboxes/{inboxId}/impressions/context:
    parameters:
      - $ref: "#/components/parameters/InboxId"
    get:
      tags: [Analytics]
      summary: Get context field breakdown
      description: Analyze a specific field from the context JSON across impressions
      operationId: getContextBreakdown
      parameters:
        - name: field
          in: query
          required: true
          schema:
            type: string
          description: The context field to analyze (e.g., "page", "userId")
        - name: days
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 30
      responses:
        "200":
          description: Context field breakdown
          content:
            application/json:
              schema:
                type: object
                properties:
                  field:
                    type: string
                  breakdown:
                    type: array
                    items:
                      type: object
                      properties:
                        value:
                          type: string
                        impressions:
                          type: integer
                        with_feedback:
                          type: integer
                        conversion_rate:
                          type: number

  # ==================== FEEDBACK PAGES ====================
  /organizations/{orgId}/feedback-pages:
    get:
      tags: [Feedback Pages]
      summary: List feedback pages
      description: Returns all feedback pages for the organization
      operationId: listFeedbackPages
      parameters:
        - $ref: "#/components/parameters/orgId"
      responses:
        "200":
          description: List of feedback pages
          content:
            application/json:
              schema:
                type: object
                properties:
                  pages:
                    type: array
                    items:
                      $ref: "#/components/schemas/FeedbackPage"
    post:
      tags: [Feedback Pages]
      summary: Create a feedback page
      description: Create a new feedback page with questions
      operationId: createFeedbackPage
      parameters:
        - $ref: "#/components/parameters/orgId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateFeedbackPage"
      responses:
        "201":
          description: Feedback page created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedbackPage"

  /organizations/{orgId}/feedback-pages/{pageId}:
    get:
      tags: [Feedback Pages]
      summary: Get a feedback page
      operationId: getFeedbackPage
      parameters:
        - $ref: "#/components/parameters/orgId"
        - name: pageId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Feedback page details with questions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedbackPage"
    put:
      tags: [Feedback Pages]
      summary: Update a feedback page
      operationId: updateFeedbackPage
      parameters:
        - $ref: "#/components/parameters/orgId"
        - name: pageId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateFeedbackPage"
      responses:
        "200":
          description: Feedback page updated
    delete:
      tags: [Feedback Pages]
      summary: Delete a feedback page
      operationId: deleteFeedbackPage
      parameters:
        - $ref: "#/components/parameters/orgId"
        - name: pageId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Feedback page deleted

  /organizations/{orgId}/feedback-pages/{pageId}/questions:
    post:
      tags: [Feedback Pages]
      summary: Add a question to a feedback page
      operationId: addFeedbackPageQuestion
      parameters:
        - $ref: "#/components/parameters/orgId"
        - name: pageId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateFeedbackPageQuestion"
      responses:
        "201":
          description: Question added

  /organizations/{orgId}/feedback-pages/{pageId}/generate:
    post:
      tags: [Feedback Pages]
      summary: AI-generate feedback page questions
      description: Uses AI to generate a feedback page title, description, and questions from a natural language prompt. Supports creating new pages and editing existing ones.
      operationId: aiGenerateFeedbackPage
      parameters:
        - $ref: "#/components/parameters/orgId"
        - name: pageId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prompt]
              properties:
                prompt:
                  type: string
                  description: Natural language description of the form to generate
                  maxLength: 2000
                existingQuestions:
                  type: array
                  description: Current questions when editing an existing page
                  items:
                    type: object
                    properties:
                      type:
                        $ref: "#/components/schemas/FeedbackPageQuestionType"
                      prompt:
                        type: string
      responses:
        "200":
          description: AI-generated page content
          content:
            application/json:
              schema:
                type: object
                properties:
                  title:
                    type: string
                  description:
                    type: string
                  questions:
                    type: array
                    items:
                      $ref: "#/components/schemas/CreateFeedbackPageQuestion"

  /p/{shortCode}:
    get:
      tags: [Feedback Pages]
      summary: Get public feedback page
      description: Returns a public feedback page by short code for rendering to end users
      operationId: getPublicFeedbackPage
      security: []
      parameters:
        - name: shortCode
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Public feedback page with questions and brand settings

  /p/{shortCode}/submit:
    post:
      tags: [Feedback Pages]
      summary: Submit feedback page responses
      description: Submit answers to a public feedback page. Display-only question types (content_block, section_header) are automatically excluded.
      operationId: submitFeedbackPage
      security: []
      parameters:
        - name: shortCode
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [answers, turnstileToken]
              properties:
                answers:
                  type: array
                  items:
                    type: object
                    required: [questionId]
                    properties:
                      questionId:
                        type: string
                      score:
                        type: number
                        description: For rating types (thumbs, stars, csat, nps, emoji)
                      textValue:
                        type: string
                        description: For text, email, phone, number, date, dropdown, single/multi choice
                contactName:
                  type: string
                contactEmail:
                  type: string
                  format: email
                turnstileToken:
                  type: string
                  description: Cloudflare Turnstile CAPTCHA token
      responses:
        "200":
          description: Submission recorded

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Pollenate-Key

  parameters:
    OrgId:
      name: orgId
      in: path
      required: true
      schema:
        type: string
      description: Organization ID
    BrandId:
      name: brandId
      in: path
      required: true
      schema:
        type: string
      description: Brand ID
    InboxId:
      name: inboxId
      in: path
      required: true
      schema:
        type: string
      description: Inbox ID
    UserId:
      name: userId
      in: path
      required: true
      schema:
        type: string
      description: User ID
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    Cursor:
      name: cursor
      in: query
      schema:
        type: string
      description: Pagination cursor

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Insufficient permissions
      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
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
        code:
          type: string
        details:
          type: object

    # Auth
    AuthTokens:
      type: object
      properties:
        accessToken:
          type: string
        refreshToken:
          type: string
        expiresIn:
          type: integer
          description: Access token expiry in seconds
        user:
          $ref: "#/components/schemas/User"

    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        avatarUrl:
          type: string
          format: uri
        createdAt:
          type: string
          format: date-time

    UserWithOrganizations:
      allOf:
        - $ref: "#/components/schemas/User"
        - type: object
          properties:
            organizations:
              type: array
              items:
                $ref: "#/components/schemas/OrganizationWithRole"

    # Organization
    Organization:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        logoUrl:
          type: string
          format: uri
        subscriptionStatus:
          type: string
          enum: [trial, active, past_due, canceled]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    OrganizationWithRole:
      allOf:
        - $ref: "#/components/schemas/Organization"
        - type: object
          properties:
            role:
              $ref: "#/components/schemas/MemberRole"

    OrganizationDetail:
      allOf:
        - $ref: "#/components/schemas/Organization"
        - type: object
          properties:
            memberCount:
              type: integer
            brandCount:
              type: integer
            settings:
              type: object

    CreateOrganization:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 100
        slug:
          type: string
          pattern: "^[a-z0-9-]+$"
          minLength: 2
          maxLength: 50
          description: URL-friendly identifier (auto-generated if not provided)

    UpdateOrganization:
      type: object
      properties:
        name:
          type: string
        logoUrl:
          type: string
          format: uri
        settings:
          type: object

    # Members
    MemberRole:
      type: string
      enum: [owner, admin, member]

    Member:
      type: object
      properties:
        id:
          type: string
        userId:
          type: string
        email:
          type: string
        name:
          type: string
        avatarUrl:
          type: string
        role:
          $ref: "#/components/schemas/MemberRole"
        joinedAt:
          type: string
          format: date-time

    # Invitations
    Invitation:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
        role:
          type: string
          enum: [admin, member]
        brandId:
          type:
            - string
            - "null"
        status:
          type: string
          enum: [pending, accepted, expired, revoked]
        invitedBy:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            email:
              type: string
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time

    InvitationPublic:
      type: object
      properties:
        organizationName:
          type: string
        brandName:
          type:
            - string
            - "null"
        invitedByName:
          type: string
        role:
          type: string
        expiresAt:
          type: string
          format: date-time

    CreateInvitation:
      type: object
      required: [email]
      properties:
        email:
          type: string
          format: email
        role:
          type: string
          enum: [admin, member]
          default: member
        brandId:
          type: string
          description: Optional brand to scope invitation to

    # Invite Links
    InviteLink:
      type: object
      properties:
        id:
          type: string
        code:
          type: string
          description: 8-character alphanumeric code used in the join URL
          example: Ab3xK7mN
        role:
          type: string
          enum: [admin, member, billing, viewer]
        label:
          type:
            - string
            - "null"
          description: Admin-facing description
        maxUses:
          type:
            - integer
            - "null"
          description: Maximum number of uses (null = unlimited)
        useCount:
          type: integer
          description: Number of times the link has been used
        expiresAt:
          type:
            - string
            - "null"
          format: date-time
          description: Expiration timestamp (null = never expires)
        isActive:
          type: boolean
        createdBy:
          type: object
          properties:
            id:
              type: string
            name:
              type:
                - string
                - "null"
            email:
              type: string
        brandIds:
          type: array
          items:
            type: string
          description: Brand IDs granted on join
        createdAt:
          type: string
          format: date-time

    InviteLinkPreview:
      type: object
      properties:
        organizationName:
          type: string
        organizationSlug:
          type: string
        role:
          type: string
        createdByName:
          type: string
        isActive:
          type: boolean
        isExpired:
          type: boolean
        isFull:
          type: boolean

    CreateInviteLink:
      type: object
      properties:
        role:
          type: string
          enum: [admin, member, billing, viewer]
          default: member
        label:
          type: string
          maxLength: 100
          description: Optional admin-facing description
        maxUses:
          type: integer
          minimum: 1
          description: Maximum number of uses (omit for unlimited)
        expiresIn:
          type: string
          enum: ["30m", "1h", "6h", "12h", "1d", "7d", "never"]
          description: Expiration preset
          default: "7d"
        brandIds:
          type: array
          items:
            type: string
          description: Brand IDs to grant access to on join

    # API Keys
    ApiKeyMeta:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        keyPrefix:
          type: string
          example: pk_live_ab
        brandId:
          type:
            - string
            - "null"
        scopes:
          type: array
          items:
            type: string
        lastUsedAt:
          type:
            - string
            - "null"
          format: date-time
        expiresAt:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type: string
          format: date-time

    ApiKeyWithSecret:
      allOf:
        - $ref: "#/components/schemas/ApiKeyMeta"
        - type: object
          properties:
            key:
              type: string
              description: Full API key (only returned on creation!)
              example: pk_live_abc123xyz789...

    CreateApiKey:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 100
        brandId:
          type: string
          description: Scope key to specific brand (null = org-wide)
        scopes:
          type: array
          items:
            type: string
            enum: ["collect", "read", "write", "admin"]
          minItems: 1
          default: ["collect"]
          description: |
            Permission scopes for the API key:
            - `collect`: Submit feedback via widget
            - `read`: Read feedback and analytics
            - `write`: Create/update inboxes and settings
            - `admin`: Full access including team management
        expiresAt:
          type: string
          format: date-time
          description: Optional expiration date

    # Brands
    Brand:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        logoUrl:
          type: string
          format: uri
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    BrandDetail:
      allOf:
        - $ref: "#/components/schemas/Brand"
        - type: object
          properties:
            memberCount:
              type: integer
            inboxCount:
              type: integer
            settings:
              type: object

    CreateBrand:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 100
        slug:
          type: string
          pattern: "^[a-z0-9-]+$"
        logoUrl:
          type: string
          format: uri

    UpdateBrand:
      type: object
      properties:
        name:
          type: string
        logoUrl:
          type: string
          format: uri
        settings:
          type: object

    # Inboxes
    Inbox:
      type: object
      properties:
        id:
          type: string
        key:
          type: string
        name:
          type: string
        description:
          type: string
        themeId:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    InboxDetail:
      allOf:
        - $ref: "#/components/schemas/Inbox"
        - type: object
          properties:
            feedbackCount:
              type: integer
            settings:
              type: object

    CreateInbox:
      type: object
      required: [name, key]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 100
        key:
          type: string
          pattern: "^[a-z0-9-_]+$"
          minLength: 2
          maxLength: 50
        description:
          type: string
          maxLength: 500
        themeId:
          type: string

    UpdateInbox:
      type: object
      properties:
        name:
          type: string
        description:
          type: string
        themeId:
          type: string
        settings:
          type: object

    # Feedback (widget types)
    FeedbackType:
      type: string
      description: Feedback type for widget-based collection
      enum: [thumbs, stars, csat, nps, emoji, text]

    # Feedback Page question types
    FeedbackPageQuestionType:
      type: string
      description: |
        Question type for feedback page forms. Includes all widget rating types plus
        structured input types and display-only layout controls.
      enum:
        - thumbs
        - stars
        - csat
        - nps
        - emoji
        - text
        - single_choice
        - multi_choice
        - content_block
        - section_header
        - email
        - phone
        - number
        - date
        - dropdown

    FeedbackPage:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: string
        brandId:
          type: string
        inboxId:
          type: string
        shortCode:
          type: string
        isActive:
          type: boolean
        displayMode:
          type: string
          enum: [all, one_at_a_time]
        introMarkdown:
          type: string
        questions:
          type: array
          items:
            $ref: "#/components/schemas/FeedbackPageQuestion"
        createdAt:
          type: string
          format: date-time

    FeedbackPageQuestion:
      type: object
      properties:
        id:
          type: string
        type:
          $ref: "#/components/schemas/FeedbackPageQuestionType"
        prompt:
          type: string
          description: The question text (or internal label for display-only types)
        description:
          type: string
          description: Optional help text shown below the question
        required:
          type: boolean
        sortOrder:
          type: integer
        config:
          $ref: "#/components/schemas/QuestionConfig"

    CreateFeedbackPage:
      type: object
      required: [title, brandId]
      properties:
        title:
          type: string
        description:
          type: string
        brandId:
          type: string
        inboxId:
          type: string
        displayMode:
          type: string
          enum: [all, one_at_a_time]
        introMarkdown:
          type: string
        questions:
          type: array
          items:
            $ref: "#/components/schemas/CreateFeedbackPageQuestion"

    UpdateFeedbackPage:
      type: object
      properties:
        title:
          type: string
        description:
          type: string
        inboxId:
          type: string
        isActive:
          type: boolean
        displayMode:
          type: string
          enum: [all, one_at_a_time]
        introMarkdown:
          type: string

    CreateFeedbackPageQuestion:
      type: object
      required: [type, prompt]
      properties:
        type:
          $ref: "#/components/schemas/FeedbackPageQuestionType"
        prompt:
          type: string
        description:
          type: string
        required:
          type: boolean
          default: false
        config:
          $ref: "#/components/schemas/QuestionConfig"

    QuestionConfig:
      type: object
      description: |
        Type-specific configuration. Fields vary by question type:
        - single_choice / multi_choice / dropdown: `options` (string array, required)
        - text: `multiline` (boolean), `validation` (email|phone|url|regex), `validationMessage`, `placeholder`
        - content_block: `markdown` (string, required)
        - email: `placeholder`
        - phone: `format` (us|international|any), `placeholder`
        - number: `min`, `max`, `step`, `unit`, `placeholder`
        - date: `includeTime` (boolean), `placeholder`
        - dropdown: `options` (string array, required), `placeholder`
      properties:
        options:
          type: array
          items:
            type: string
          description: Options list for choice/dropdown types
        multiline:
          type: boolean
          description: "Text type: true for textarea, false for single-line input"
        markdown:
          type: string
          description: "Content block: markdown content to render"
        placeholder:
          type: string
          description: Placeholder text for input fields
        validation:
          type: string
          enum: [email, phone, url, regex]
          description: "Text type: validation rule"
        validationRegex:
          type: string
          description: "Custom regex when validation is 'regex'"
        validationMessage:
          type: string
          description: Custom error message for validation failures
        format:
          type: string
          enum: [us, international, any]
          description: "Phone type: number format"
        min:
          type: number
          description: "Number type: minimum value"
        max:
          type: number
          description: "Number type: maximum value"
        step:
          type: number
          description: "Number type: step increment"
        unit:
          type: string
          description: "Number type: unit label (e.g. 'kg', '$')"
        includeTime:
          type: boolean
          description: "Date type: include time picker"

    FeedbackSubmission:
      type: object
      required: [inboxKey, type]
      properties:
        inboxKey:
          type: string
        type:
          $ref: "#/components/schemas/FeedbackType"
        score:
          type: number
          minimum: 0
          maximum: 10
        comment:
          type: string
          maxLength: 2000
        context:
          type: object
          description: Arbitrary metadata (pageUrl, customerId, etc.)
          additionalProperties: true
        impressionId:
          type: string
          description: Optional ID of the tracked impression to link this feedback to
        sessionId:
          type: string
          description: Session identifier for correlating events
        pageUrl:
          type: string
          description: URL of the page where feedback was submitted

    ImpressionSubmission:
      type: object
      required: [inboxKey]
      properties:
        inboxKey:
          type: string
          description: The inbox key to track the impression for
        context:
          type: object
          description: Arbitrary metadata (page, userId, etc.)
          additionalProperties: true
        sessionId:
          type: string
          description: Session identifier for correlating events
        pageUrl:
          type: string
          description: URL of the page where widget loaded
        referrer:
          type: string
          description: Referrer URL
        userAgent:
          type: string
          description: Browser user agent
        externalUserId:
          type: string
          description: External user ID (also extracted from context.userId if not provided)

    AnalyticsSummary:
      type: object
      properties:
        totals:
          type: object
          properties:
            impressions:
              type: integer
            withFeedback:
              type: integer
            conversionRate:
              type: number
            uniqueUsers:
              type: integer
        byInbox:
          type: array
          items:
            type: object
            properties:
              inbox_id:
                type: string
              inbox_name:
                type: string
              brand_name:
                type: string
              total_impressions:
                type: integer
              impressions_with_feedback:
                type: integer
              conversion_rate:
                type: number
              unique_users:
                type: integer
        period:
          type: object
          properties:
            start:
              type: string
              format: date
            end:
              type: string
              format: date
            days:
              type: integer

    InboxImpressionAnalytics:
      type: object
      properties:
        summary:
          type: object
          properties:
            totalImpressions:
              type: integer
            impressionsWithFeedback:
              type: integer
            conversionRate:
              type: number
            uniqueUsers:
              type: integer
            uniqueSessions:
              type: integer
            period:
              type: object
              properties:
                start:
                  type: string
                  format: date
                end:
                  type: string
                  format: date
                days:
                  type: integer
        timeSeries:
          type: array
          items:
            type: object
            properties:
              period:
                type: string
              impressions:
                type: integer
              with_feedback:
                type: integer
              unique_users:
                type: integer
        topPages:
          type: array
          items:
            type: object
            properties:
              page_url:
                type: string
              impressions:
                type: integer
              with_feedback:
                type: integer
              conversion_rate:
                type: number
        topUsers:
          type: array
          items:
            type: object
            properties:
              external_user_id:
                type: string
              impressions:
                type: integer
              feedback_count:
                type: integer
              first_seen:
                type: string
                format: date-time
              last_seen:
                type: string
                format: date-time

    FeedbackEvent:
      type: object
      properties:
        id:
          type: string
        inboxId:
          type: string
        type:
          $ref: "#/components/schemas/FeedbackType"
        score:
          type: number
        comment:
          type: string
        context:
          type: object
        userAgent:
          type: string
        createdAt:
          type: string
          format: date-time

    FeedbackList:
      type: object
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/FeedbackEvent"
        cursor:
          type:
            - string
            - "null"
        hasMore:
          type: boolean

    # Search
    SearchResult:
      type: object
      properties:
        feedback:
          $ref: "#/components/schemas/FeedbackEvent"
        score:
          type: number
          description: Similarity score (0-1)
        highlights:
          type: array
          items:
            type: string
          description: Relevant text snippets

    # Themes
    Theme:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        tokens:
          $ref: "#/components/schemas/ThemeTokens"
        isPublic:
          type: boolean
        createdAt:
          type: string
          format: date-time

    ThemeTokens:
      type: object
      properties:
        colorPrimary:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
        colorAccent:
          type: string
        colorBackground:
          type: string
        colorText:
          type: string
        radius:
          type: string
        shadow:
          type: string
        fontFamily:
          type: string

security:
  - BearerAuth: []
