openapi: 3.1.0
info:
  title: Api
  version: 0.1.0
  description: Top Real Estate Tools API
servers:
  - url: /api
    description: Base API path
tags:
  - name: health
  - name: public
  - name: submissions
  - name: upvotes
  - name: admin
  - name: Auth
    description: Browser and mobile authentication endpoints.
paths:
  /auth/user:
    get:
      tags: [Auth]
      operationId: getCurrentAuthUser
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthUserEnvelope" }
  /login:
    get:
      tags: [Auth]
      operationId: beginBrowserLogin
      parameters:
        - in: query
          name: returnTo
          schema: { type: string }
      responses:
        "302": { description: Redirect to OIDC }
  /callback:
    get:
      tags: [Auth]
      operationId: handleBrowserLoginCallback
      parameters:
        - in: query
          name: code
          schema: { type: string }
        - in: query
          name: state
          schema: { type: string }
      responses:
        "302": { description: Redirect after login }
  /logout:
    get:
      tags: [Auth]
      operationId: logoutBrowserSession
      responses:
        "302": { description: Redirect to OIDC end session }
  /mobile-auth/token-exchange:
    post:
      tags: [Auth]
      operationId: exchangeMobileAuthorizationCode
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MobileTokenExchangeRequest" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MobileTokenExchangeSuccess" }
  /mobile-auth/logout:
    post:
      tags: [Auth]
      operationId: logoutMobileSession
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LogoutSuccess" }

  /healthz:
    get:
      operationId: healthCheck
      tags: [health]
      summary: Health check
      responses:
        "200":
          description: Healthy
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HealthStatus" }

  /site:
    get:
      operationId: getSiteConfig
      tags: [public]
      summary: Public site settings
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SiteConfig" }

  /homepage:
    get:
      operationId: getHomepage
      tags: [public]
      summary: Homepage aggregated payload
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HomepagePayload" }

  /lists:
    get:
      operationId: listTopLists
      tags: [public]
      parameters:
        - in: query
          name: published
          schema: { type: boolean }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/TopListSummary" }

  /lists/{slug}:
    get:
      operationId: getTopList
      tags: [public]
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TopListDetail" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /categories:
    get:
      operationId: listCategories
      tags: [public]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Category" }

  /categories/{slug}:
    get:
      operationId: getCategory
      tags: [public]
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CategoryDetail" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /companies:
    get:
      operationId: listCompanies
      tags: [public]
      parameters:
        - in: query
          name: q
          schema: { type: string }
        - in: query
          name: category
          schema: { type: string }
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/CompanySummary" }

  /companies/{slug}:
    get:
      operationId: getCompany
      tags: [public]
      parameters:
        - in: path
          name: slug
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompanyDetail" }
        "404":
          description: Not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /companies/submit:
    post:
      operationId: submitCompany
      tags: [submissions]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CompanySubmissionInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SubmissionResult" }

  /verify/{token}:
    get:
      operationId: verifyToken
      tags: [submissions]
      parameters:
        - in: path
          name: token
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VerificationResult" }

  /upvotes:
    post:
      operationId: requestUpvote
      tags: [upvotes]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpvoteInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UpvoteResult" }

  /claims:
    post:
      operationId: submitClaim
      tags: [submissions]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ClaimInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SubmissionResult" }

  /suggestions:
    post:
      operationId: submitSuggestion
      tags: [submissions]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SuggestionInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SubmissionResult" }

  /newsletter:
    post:
      operationId: subscribeNewsletter
      tags: [submissions]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NewsletterInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SubmissionResult" }

  /outbound-click:
    post:
      operationId: trackOutboundClick
      tags: [public]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OutboundClickInput" }
      responses:
        "201":
          description: Tracked
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SimpleOk" }

  /faqs:
    get:
      operationId: listFaqs
      tags: [public]
      parameters:
        - in: query
          name: location
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Faq" }

  # ----- ADMIN -----
  /admin/me:
    get:
      operationId: getAdminMe
      tags: [admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminUser" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /admin/stats:
    get:
      operationId: getAdminStats
      tags: [admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AdminStats" }

  /admin/companies:
    get:
      operationId: adminListCompanies
      tags: [admin]
      parameters:
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: q
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/CompanyAdmin" }
    post:
      operationId: adminCreateCompany
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CompanyInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompanyAdmin" }

  /admin/companies/{id}:
    patch:
      operationId: adminUpdateCompany
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CompanyUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompanyAdmin" }
    delete:
      operationId: adminDeleteCompany
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SimpleOk" }

  /admin/lists:
    get:
      operationId: adminListLists
      tags: [admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/TopListAdmin" }
    post:
      operationId: adminCreateList
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TopListInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TopListAdmin" }

  /admin/lists/{id}:
    patch:
      operationId: adminUpdateList
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TopListUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TopListAdmin" }
    delete:
      operationId: adminDeleteList
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SimpleOk" }

  /admin/lists/{id}/entries:
    put:
      operationId: adminSetListEntries
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ListEntriesInput" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TopListAdmin" }

  /admin/categories:
    get:
      operationId: adminListCategories
      tags: [admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Category" }
    post:
      operationId: adminCreateCategory
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CategoryInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }

  /admin/categories/{id}:
    patch:
      operationId: adminUpdateCategory
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CategoryUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Category" }
    delete:
      operationId: adminDeleteCategory
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SimpleOk" }

  /admin/submissions:
    get:
      operationId: adminListSubmissions
      tags: [admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/CompanyAdmin" }

  /admin/submissions/{id}/approve:
    post:
      operationId: adminApproveSubmission
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompanyAdmin" }

  /admin/submissions/{id}/reject:
    post:
      operationId: adminRejectSubmission
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompanyAdmin" }

  /admin/leads:
    get:
      operationId: adminListLeads
      tags: [admin]
      parameters:
        - in: query
          name: type
          schema: { type: string }
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: q
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Lead" }

  /admin/leads/{id}:
    patch:
      operationId: adminUpdateLead
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LeadUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Lead" }

  /admin/upvotes:
    get:
      operationId: adminListUpvotes
      tags: [admin]
      parameters:
        - in: query
          name: companyId
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/UpvoteRecord" }

  /admin/faqs:
    get:
      operationId: adminListFaqs
      tags: [admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Faq" }
    post:
      operationId: adminCreateFaq
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FaqInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Faq" }

  /admin/faqs/{id}:
    patch:
      operationId: adminUpdateFaq
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FaqUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Faq" }
    delete:
      operationId: adminDeleteFaq
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SimpleOk" }

  /admin/placements:
    get:
      operationId: adminListPlacements
      tags: [admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/OwnedPlacement" }
    post:
      operationId: adminCreatePlacement
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OwnedPlacementInput" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OwnedPlacement" }

  /admin/placements/{id}:
    patch:
      operationId: adminUpdatePlacement
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OwnedPlacementUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OwnedPlacement" }
    delete:
      operationId: adminDeletePlacement
      tags: [admin]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SimpleOk" }

  /admin/settings:
    get:
      operationId: adminGetSettings
      tags: [admin]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SiteSettings" }
    patch:
      operationId: adminUpdateSettings
      tags: [admin]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SiteSettingsUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SiteSettings" }

components:
  schemas:
    AuthUser:
      type: object
      required: [id, email, firstName, lastName, profileImageUrl]
      properties:
        id: { type: string }
        email: { type: ["string", "null"] }
        firstName: { type: ["string", "null"] }
        lastName: { type: ["string", "null"] }
        profileImageUrl: { type: ["string", "null"] }
    AuthUserEnvelope:
      type: object
      required: [user]
      properties:
        user:
          oneOf:
            - $ref: "#/components/schemas/AuthUser"
            - type: "null"
    MobileTokenExchangeRequest:
      type: object
      required: [code, code_verifier, redirect_uri, state]
      properties:
        code: { type: string }
        code_verifier: { type: string }
        redirect_uri: { type: string }
        state: { type: string }
        nonce: { type: string }
    MobileTokenExchangeSuccess:
      type: object
      required: [token]
      properties:
        token: { type: string }
    LogoutSuccess:
      type: object
      required: [success]
      properties:
        success: { type: boolean }

    HealthStatus:
      type: object
      properties:
        status: { type: string }
      required: [status]

    ErrorResponse:
      type: object
      properties:
        error: { type: string }
      required: [error]

    SimpleOk:
      type: object
      properties:
        ok: { type: boolean }
      required: [ok]

    SiteConfig:
      type: object
      required: [siteName, domain, vertical, audience, primaryListConcept, brandColors]
      properties:
        siteName: { type: string }
        domain: { type: string }
        vertical: { type: string }
        audience: { type: string }
        primaryListConcept: { type: string }
        homepageHeadline: { type: string }
        homepageSubheadline: { type: string }
        methodology: { type: string }
        brandColors:
          type: object
          required: [navy, accent, highlight, neutral]
          properties:
            navy: { type: string }
            accent: { type: string }
            highlight: { type: string }
            neutral: { type: string }

    Category:
      type: object
      required: [id, slug, name, description, companyCount, listCount]
      properties:
        id: { type: integer }
        slug: { type: string }
        name: { type: string }
        description: { type: string }
        parentId: { type: ["integer", "null"] }
        seoTitle: { type: ["string", "null"] }
        seoDescription: { type: ["string", "null"] }
        companyCount: { type: integer }
        listCount: { type: integer }

    CategoryDetail:
      type: object
      required: [category, companies, lists, faqs]
      properties:
        category: { $ref: "#/components/schemas/Category" }
        companies:
          type: array
          items: { $ref: "#/components/schemas/CompanySummary" }
        lists:
          type: array
          items: { $ref: "#/components/schemas/TopListSummary" }
        faqs:
          type: array
          items: { $ref: "#/components/schemas/Faq" }

    CategoryInput:
      type: object
      required: [name, slug, description]
      properties:
        name: { type: string }
        slug: { type: string }
        description: { type: string }
        parentId: { type: ["integer", "null"] }
        seoTitle: { type: ["string", "null"] }
        seoDescription: { type: ["string", "null"] }

    CategoryUpdate:
      type: object
      properties:
        name: { type: string }
        slug: { type: string }
        description: { type: string }
        parentId: { type: ["integer", "null"] }
        seoTitle: { type: ["string", "null"] }
        seoDescription: { type: ["string", "null"] }

    EditorialBadge:
      type: string
      description: Editorial badge label

    CompanySummary:
      type: object
      required: [id, slug, name, shortDescription, upvoteCount, status]
      properties:
        id: { type: integer }
        slug: { type: string }
        name: { type: string }
        website: { type: ["string", "null"] }
        logoUrl: { type: ["string", "null"] }
        shortDescription: { type: string }
        bestFor: { type: ["string", "null"] }
        pricingModel: { type: ["string", "null"] }
        categorySlug: { type: ["string", "null"] }
        categoryName: { type: ["string", "null"] }
        upvoteCount: { type: integer }
        status: { type: string }
        badges:
          type: array
          items: { type: string }
        features:
          type: array
          items: { type: string }

    CompanyDetail:
      type: object
      required: [company, relatedCompanies, relatedLists, faqs]
      properties:
        company: { $ref: "#/components/schemas/CompanyFull" }
        relatedCompanies:
          type: array
          items: { $ref: "#/components/schemas/CompanySummary" }
        relatedLists:
          type: array
          items: { $ref: "#/components/schemas/TopListSummary" }
        faqs:
          type: array
          items: { $ref: "#/components/schemas/Faq" }

    CompanyFull:
      type: object
      required: [id, slug, name, shortDescription, longDescription, status, upvoteCount]
      properties:
        id: { type: integer }
        slug: { type: string }
        name: { type: string }
        website: { type: ["string", "null"] }
        logoUrl: { type: ["string", "null"] }
        shortDescription: { type: string }
        longDescription: { type: string }
        categoryId: { type: ["integer", "null"] }
        categorySlug: { type: ["string", "null"] }
        categoryName: { type: ["string", "null"] }
        pricingModel: { type: ["string", "null"] }
        bestFor: { type: ["string", "null"] }
        targetCustomers: { type: ["string", "null"] }
        location: { type: ["string", "null"] }
        foundedYear: { type: ["integer", "null"] }
        status: { type: string }
        editorialStatus: { type: string }
        upvoteCount: { type: integer }
        outboundClickCount: { type: integer }
        leadCount: { type: integer }
        features:
          type: array
          items: { type: string }
        useCases:
          type: array
          items: { type: string }
        tags:
          type: array
          items: { type: string }
        badges:
          type: array
          items: { type: string }
        screenshots:
          type: array
          items: { type: string }
        socialLinks:
          type: object
          additionalProperties: { type: string }
        lastUpdated: { type: string }

    CompanyAdmin:
      allOf:
        - $ref: "#/components/schemas/CompanyFull"
        - type: object
          properties:
            adminNotes: { type: ["string", "null"] }
            submitterEmail: { type: ["string", "null"] }
            submitterEmailVerified: { type: boolean }
            createdAt: { type: string }

    CompanyInput:
      type: object
      required: [name, slug, shortDescription, longDescription]
      properties:
        name: { type: string }
        slug: { type: string }
        website: { type: ["string", "null"] }
        logoUrl: { type: ["string", "null"] }
        shortDescription: { type: string }
        longDescription: { type: string }
        categoryId: { type: ["integer", "null"] }
        pricingModel: { type: ["string", "null"] }
        bestFor: { type: ["string", "null"] }
        targetCustomers: { type: ["string", "null"] }
        location: { type: ["string", "null"] }
        foundedYear: { type: ["integer", "null"] }
        status: { type: ["string", "null"] }
        editorialStatus: { type: ["string", "null"] }
        features:
          type: array
          items: { type: string }
        useCases:
          type: array
          items: { type: string }
        tags:
          type: array
          items: { type: string }
        badges:
          type: array
          items: { type: string }
        adminNotes: { type: ["string", "null"] }

    CompanyUpdate:
      type: object
      properties:
        name: { type: string }
        slug: { type: string }
        website: { type: ["string", "null"] }
        logoUrl: { type: ["string", "null"] }
        shortDescription: { type: string }
        longDescription: { type: string }
        categoryId: { type: ["integer", "null"] }
        pricingModel: { type: ["string", "null"] }
        bestFor: { type: ["string", "null"] }
        targetCustomers: { type: ["string", "null"] }
        location: { type: ["string", "null"] }
        foundedYear: { type: ["integer", "null"] }
        status: { type: string }
        editorialStatus: { type: string }
        features:
          type: array
          items: { type: string }
        useCases:
          type: array
          items: { type: string }
        tags:
          type: array
          items: { type: string }
        badges:
          type: array
          items: { type: string }
        adminNotes: { type: ["string", "null"] }

    TopListSummary:
      type: object
      required: [id, slug, title, intro, companyCount, published]
      properties:
        id: { type: integer }
        slug: { type: string }
        title: { type: string }
        intro: { type: string }
        categoryId: { type: ["integer", "null"] }
        categoryName: { type: ["string", "null"] }
        categorySlug: { type: ["string", "null"] }
        companyCount: { type: integer }
        published: { type: boolean }
        editorName: { type: ["string", "null"] }
        lastUpdated: { type: string }

    TopListDetail:
      type: object
      required: [list, entries, communitySubmissions, faqs, relatedLists]
      properties:
        list: { $ref: "#/components/schemas/TopListFull" }
        entries:
          type: array
          items: { $ref: "#/components/schemas/TopListEntry" }
        communitySubmissions:
          type: array
          items: { $ref: "#/components/schemas/CompanySummary" }
        faqs:
          type: array
          items: { $ref: "#/components/schemas/Faq" }
        relatedLists:
          type: array
          items: { $ref: "#/components/schemas/TopListSummary" }
        ownedPlacements:
          type: array
          items: { $ref: "#/components/schemas/OwnedPlacement" }

    TopListFull:
      type: object
      required: [id, slug, title, intro, methodology, published]
      properties:
        id: { type: integer }
        slug: { type: string }
        title: { type: string }
        intro: { type: string }
        methodology: { type: string }
        seoTitle: { type: ["string", "null"] }
        seoDescription: { type: ["string", "null"] }
        categoryId: { type: ["integer", "null"] }
        categoryName: { type: ["string", "null"] }
        categorySlug: { type: ["string", "null"] }
        editorName: { type: ["string", "null"] }
        published: { type: boolean }
        lastUpdated: { type: string }

    TopListAdmin:
      allOf:
        - $ref: "#/components/schemas/TopListFull"
        - type: object
          properties:
            entries:
              type: array
              items: { $ref: "#/components/schemas/TopListEntry" }

    TopListEntry:
      type: object
      required: [rank, company]
      properties:
        rank: { type: integer }
        editorialNote: { type: ["string", "null"] }
        company: { $ref: "#/components/schemas/CompanySummary" }

    TopListInput:
      type: object
      required: [title, slug, intro]
      properties:
        title: { type: string }
        slug: { type: string }
        intro: { type: string }
        methodology: { type: ["string", "null"] }
        seoTitle: { type: ["string", "null"] }
        seoDescription: { type: ["string", "null"] }
        categoryId: { type: ["integer", "null"] }
        editorName: { type: ["string", "null"] }
        published: { type: ["boolean", "null"] }

    TopListUpdate:
      type: object
      properties:
        title: { type: string }
        slug: { type: string }
        intro: { type: string }
        methodology: { type: string }
        seoTitle: { type: ["string", "null"] }
        seoDescription: { type: ["string", "null"] }
        categoryId: { type: ["integer", "null"] }
        editorName: { type: ["string", "null"] }
        published: { type: boolean }

    ListEntriesInput:
      type: object
      required: [entries]
      properties:
        entries:
          type: array
          items:
            type: object
            required: [companyId, rank]
            properties:
              companyId: { type: integer }
              rank: { type: integer }
              editorialNote: { type: ["string", "null"] }

    CompanySubmissionInput:
      type: object
      required: [companyName, website, submitterName, submitterEmail, description, categoryId, consent]
      properties:
        companyName: { type: string }
        website: { type: string }
        submitterName: { type: string }
        submitterEmail: { type: string }
        submitterRole: { type: ["string", "null"] }
        description: { type: string }
        categoryId: { type: integer }
        subcategory: { type: ["string", "null"] }
        bestFitListId: { type: ["integer", "null"] }
        logoUrl: { type: ["string", "null"] }
        pricingModel: { type: ["string", "null"] }
        targetCustomers: { type: ["string", "null"] }
        keyFeatures:
          type: array
          items: { type: string }
        competitors: { type: ["string", "null"] }
        whyInclude: { type: ["string", "null"] }
        offer: { type: ["string", "null"] }
        sourcePage: { type: ["string", "null"] }
        utmSource: { type: ["string", "null"] }
        utmMedium: { type: ["string", "null"] }
        utmCampaign: { type: ["string", "null"] }
        honeypot: { type: ["string", "null"] }
        consent: { type: boolean }

    SubmissionResult:
      type: object
      required: [ok, message]
      properties:
        ok: { type: boolean }
        message: { type: string }
        verificationUrl: { type: ["string", "null"] }
        companySlug: { type: ["string", "null"] }

    VerificationResult:
      type: object
      required: [ok, kind, message]
      properties:
        ok: { type: boolean }
        kind: { type: string }
        message: { type: string }
        companySlug: { type: ["string", "null"] }

    UpvoteInput:
      type: object
      required: [companyId, email]
      properties:
        companyId: { type: integer }
        email: { type: string }
        useCase: { type: ["string", "null"] }
        sourcePage: { type: ["string", "null"] }
        utmSource: { type: ["string", "null"] }
        utmMedium: { type: ["string", "null"] }
        utmCampaign: { type: ["string", "null"] }
        honeypot: { type: ["string", "null"] }

    UpvoteResult:
      type: object
      required: [ok, message, status]
      properties:
        ok: { type: boolean }
        status: { type: string }
        message: { type: string }
        verificationUrl: { type: ["string", "null"] }

    UpvoteRecord:
      type: object
      required: [id, companyId, email, verified, createdAt]
      properties:
        id: { type: integer }
        companyId: { type: integer }
        companyName: { type: ["string", "null"] }
        email: { type: string }
        verified: { type: boolean }
        useCase: { type: ["string", "null"] }
        sourcePage: { type: ["string", "null"] }
        utmSource: { type: ["string", "null"] }
        createdAt: { type: string }

    ClaimInput:
      type: object
      required: [companyId, name, email, role, reason]
      properties:
        companyId: { type: integer }
        name: { type: string }
        email: { type: string }
        role: { type: string }
        companyName: { type: ["string", "null"] }
        website: { type: ["string", "null"] }
        reason: { type: string }
        requestedEdits: { type: ["string", "null"] }
        honeypot: { type: ["string", "null"] }

    SuggestionInput:
      type: object
      required: [companyId, email, correction]
      properties:
        companyId: { type: integer }
        email: { type: string }
        correction: { type: string }
        sourceUrl: { type: ["string", "null"] }
        honeypot: { type: ["string", "null"] }

    NewsletterInput:
      type: object
      required: [email]
      properties:
        email: { type: string }
        interest: { type: ["string", "null"] }
        sourcePage: { type: ["string", "null"] }
        honeypot: { type: ["string", "null"] }

    OutboundClickInput:
      type: object
      required: [companyId]
      properties:
        companyId: { type: integer }
        sourcePage: { type: ["string", "null"] }
        ctaLabel: { type: ["string", "null"] }

    Faq:
      type: object
      required: [id, question, answer, location, sortOrder]
      properties:
        id: { type: integer }
        question: { type: string }
        answer: { type: string }
        location: { type: string }
        sortOrder: { type: integer }

    FaqInput:
      type: object
      required: [question, answer, location]
      properties:
        question: { type: string }
        answer: { type: string }
        location: { type: string }
        sortOrder: { type: ["integer", "null"] }

    FaqUpdate:
      type: object
      properties:
        question: { type: string }
        answer: { type: string }
        location: { type: string }
        sortOrder: { type: integer }

    Lead:
      type: object
      required: [id, email, leadType, status, createdAt]
      properties:
        id: { type: integer }
        email: { type: string }
        name: { type: ["string", "null"] }
        companyName: { type: ["string", "null"] }
        role: { type: ["string", "null"] }
        leadType: { type: string }
        interestedCategoryId: { type: ["integer", "null"] }
        interestedCompanyId: { type: ["integer", "null"] }
        interestedCompanyName: { type: ["string", "null"] }
        sourcePage: { type: ["string", "null"] }
        utmSource: { type: ["string", "null"] }
        utmMedium: { type: ["string", "null"] }
        utmCampaign: { type: ["string", "null"] }
        status: { type: string }
        notes: { type: ["string", "null"] }
        createdAt: { type: string }

    LeadUpdate:
      type: object
      properties:
        status: { type: string }
        notes: { type: ["string", "null"] }

    HomepagePayload:
      type: object
      required: [featuredLists, popularCategories, recentCompanies, totals, faqs]
      properties:
        featuredLists:
          type: array
          items: { $ref: "#/components/schemas/TopListSummary" }
        popularCategories:
          type: array
          items: { $ref: "#/components/schemas/Category" }
        recentCompanies:
          type: array
          items: { $ref: "#/components/schemas/CompanySummary" }
        ownedPlacements:
          type: array
          items: { $ref: "#/components/schemas/OwnedPlacement" }
        faqs:
          type: array
          items: { $ref: "#/components/schemas/Faq" }
        totals:
          type: object
          required: [companies, lists, categories, upvotes]
          properties:
            companies: { type: integer }
            lists: { type: integer }
            categories: { type: integer }
            upvotes: { type: integer }

    OwnedPlacement:
      type: object
      required: [id, projectName, url, description, placementType, ctaText, disclosureText, active]
      properties:
        id: { type: integer }
        projectName: { type: string }
        url: { type: string }
        description: { type: string }
        logoUrl: { type: ["string", "null"] }
        categories:
          type: array
          items: { type: string }
        placementType: { type: string }
        ctaText: { type: string }
        disclosureText: { type: string }
        active: { type: boolean }

    OwnedPlacementInput:
      type: object
      required: [projectName, url, description, placementType, ctaText, disclosureText]
      properties:
        projectName: { type: string }
        url: { type: string }
        description: { type: string }
        logoUrl: { type: ["string", "null"] }
        categories:
          type: array
          items: { type: string }
        placementType: { type: string }
        ctaText: { type: string }
        disclosureText: { type: string }
        active: { type: ["boolean", "null"] }

    OwnedPlacementUpdate:
      type: object
      properties:
        projectName: { type: string }
        url: { type: string }
        description: { type: string }
        logoUrl: { type: ["string", "null"] }
        categories:
          type: array
          items: { type: string }
        placementType: { type: string }
        ctaText: { type: string }
        disclosureText: { type: string }
        active: { type: boolean }

    AdminUser:
      type: object
      required: [email, isAdmin]
      properties:
        email: { type: string }
        name: { type: ["string", "null"] }
        isAdmin: { type: boolean }

    AdminStats:
      type: object
      required: [totals, weekly, topPages, topCompaniesByLeads, topCompaniesByClicks, topCategories]
      properties:
        totals:
          type: object
          required: [companies, approvedCompanies, pendingSubmissions, lists, categories, leads, verifiedUpvotes, profileClaims, suggestedEdits, outboundClicks]
          properties:
            companies: { type: integer }
            approvedCompanies: { type: integer }
            pendingSubmissions: { type: integer }
            lists: { type: integer }
            categories: { type: integer }
            leads: { type: integer }
            verifiedUpvotes: { type: integer }
            profileClaims: { type: integer }
            suggestedEdits: { type: integer }
            outboundClicks: { type: integer }
        weekly:
          type: object
          required: [leads, upvotes, submissions, outboundClicks]
          properties:
            leads: { type: integer }
            upvotes: { type: integer }
            submissions: { type: integer }
            outboundClicks: { type: integer }
        leadsOverTime:
          type: array
          items:
            type: object
            required: [date, count]
            properties:
              date: { type: string }
              count: { type: integer }
        upvotesOverTime:
          type: array
          items:
            type: object
            required: [date, count]
            properties:
              date: { type: string }
              count: { type: integer }
        submissionsOverTime:
          type: array
          items:
            type: object
            required: [date, count]
            properties:
              date: { type: string }
              count: { type: integer }
        outboundClicksOverTime:
          type: array
          items:
            type: object
            required: [date, count]
            properties:
              date: { type: string }
              count: { type: integer }
        topPages:
          type: array
          items:
            type: object
            required: [page, leads]
            properties:
              page: { type: string }
              leads: { type: integer }
        topCompaniesByLeads:
          type: array
          items:
            type: object
            required: [companyId, companyName, leads]
            properties:
              companyId: { type: integer }
              companyName: { type: string }
              leads: { type: integer }
        topCompaniesByClicks:
          type: array
          items:
            type: object
            required: [companyId, companyName, clicks]
            properties:
              companyId: { type: integer }
              companyName: { type: string }
              clicks: { type: integer }
        topCategories:
          type: array
          items:
            type: object
            required: [categoryId, categoryName, leads]
            properties:
              categoryId: { type: integer }
              categoryName: { type: string }
              leads: { type: integer }

    SiteSettings:
      type: object
      required: [siteName, domain, vertical, audience, homepageHeadline, homepageSubheadline, methodology, defaultSeoTitle, defaultSeoDescription, emailFromAddress, adminEmail, footerLinks, socialLinks, llmsTxt, brandColors]
      properties:
        siteName: { type: string }
        domain: { type: string }
        vertical: { type: string }
        audience: { type: string }
        homepageHeadline: { type: string }
        homepageSubheadline: { type: string }
        methodology: { type: string }
        defaultSeoTitle: { type: string }
        defaultSeoDescription: { type: string }
        emailFromAddress: { type: string }
        adminEmail: { type: string }
        brandColors:
          type: object
          required: [navy, accent, highlight, neutral]
          properties:
            navy: { type: string }
            accent: { type: string }
            highlight: { type: string }
            neutral: { type: string }
        footerLinks:
          type: array
          items:
            type: object
            required: [label, url]
            properties:
              label: { type: string }
              url: { type: string }
        socialLinks:
          type: array
          items:
            type: object
            required: [label, url]
            properties:
              label: { type: string }
              url: { type: string }
        llmsTxt: { type: string }

    SiteSettingsUpdate:
      type: object
      properties:
        siteName: { type: string }
        domain: { type: string }
        vertical: { type: string }
        audience: { type: string }
        homepageHeadline: { type: string }
        homepageSubheadline: { type: string }
        methodology: { type: string }
        defaultSeoTitle: { type: string }
        defaultSeoDescription: { type: string }
        emailFromAddress: { type: string }
        adminEmail: { type: string }
        brandColors:
          type: object
          properties:
            navy: { type: string }
            accent: { type: string }
            highlight: { type: string }
            neutral: { type: string }
        footerLinks:
          type: array
          items:
            type: object
            required: [label, url]
            properties:
              label: { type: string }
              url: { type: string }
        socialLinks:
          type: array
          items:
            type: object
            required: [label, url]
            properties:
              label: { type: string }
              url: { type: string }
        llmsTxt: { type: string }
