openapi: 3.1.0
info:
  title: FinSEO API
  description: Access AI visibility tracking data for your brand across ChatGPT, Claude, Perplexity and other AI platforms.
  version: 1.0.0
  contact:
    name: FinSEO Support
    url: https://finseo.ai
servers:
  - url: https://api.finseo.ai/v1
    description: Production
  - url: https://app.finseo.ai/api/v1
    description: Dashboard host — same API, kept for backwards compatibility

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key starting with sk_live_ or sk_test_"

  parameters:
    projectId:
      name: projectId
      in: path
      required: true
      schema:
        type: string
    timeframe:
      name: timeframe
      in: query
      schema:
        type: string
        default: "30d"
        description: "Relative time window ending yesterday (e.g. 7d, 30d, 90d). Ignored when startDate/endDate are set."
    startDate:
      name: startDate
      in: query
      schema:
        type: string
        format: date
        description: "Custom range start (YYYY-MM-DD). Must be combined with endDate; overrides the relative timeframe."
    endDate:
      name: endDate
      in: query
      schema:
        type: string
        format: date
        description: "Custom range end (YYYY-MM-DD). Must be combined with startDate; overrides the relative timeframe."
    model:
      name: model
      in: query
      schema:
        type: string
        description: "AI model filter (chatgpt, claude, perplexity)"
    language:
      name: language
      in: query
      schema:
        type: string
        description: "Language code filter (en, de, fr)"
    tags:
      name: tags
      in: query
      schema:
        type: string
        description: "Comma-separated tags or JSON array"
    page:
      name: page
      in: query
      schema:
        type: integer
        default: 1
        minimum: 1
    limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 50
        minimum: 1
        maximum: 500

  schemas:
    PeriodMetrics:
      type: object
      description: Aggregates for one period. All rates share the denominator totalResponses; an answer can count toward both mentionRate and citationRate, so responseVisibilityRate <= mentionRate + citationRate.
      properties:
        date: { type: string, format: date, description: "End date of the period" }
        totalResponses: { type: integer, description: "Analyzed AI answers in the period" }
        responseVisibilityRate: { type: number, description: "Visibility — % of answers where the brand is visible: named in the answer text OR its own domain cited as a source (dashboard Visibility KPI)" }
        mentionRate: { type: number, description: "Mention Rate — % of answers naming the brand in the answer text" }
        citationRate: { type: number, description: "Citation Rate — % of answers citing the own domain or a configured domain alias as a source" }
        mentions: { type: integer, description: "Answers naming the brand in the text (count behind mentionRate)" }
        cited: { type: integer, description: "Answers citing the own domain as a source (count behind citationRate)" }
        visibilityRate: { type: number, description: "Prompt coverage — % of tracked prompts with at least one visible answer" }
        visible: { type: integer, description: "Prompts with at least one visible answer" }
        notVisible: { type: integer, description: "Prompts without a visible answer" }
        position: { type: number, nullable: true, description: "Mention depth — how deep in the answer text the mention appears (0 = top); not a ranking" }
        sentiment: { type: number, nullable: true, description: "Average sentiment score 0–100 across visible answers" }
    TimeseriesDay:
      type: object
      description: One calendar day. All counts are AI answers of that day.
      properties:
        date: { type: string, format: date }
        totalResults: { type: integer, description: "Analyzed AI answers" }
        visible: { type: integer, description: "Answers where the brand is visible (named in the text OR own domain cited); visible / totalResults = Visibility" }
        notVisible: { type: integer }
        mentions: { type: integer, description: "Answers naming the brand in the text; mentions / totalResults = Mention Rate" }
        cited: { type: integer, nullable: true, description: "Answers citing the own domain as a source; cited / totalResults = Citation Rate. Null when the day was served from a rollup without citations" }
        mentionOrder: { type: number, nullable: true, description: "Average ordinal position among brands named (1 = named first; dashboard Position KPI)" }
        position: { type: number, nullable: true, description: "Mention depth (0 = top of the answer text)" }
        sentiment: { type: number, nullable: true, description: "Average sentiment score 0–100" }
        uniquePrompts: { type: integer, description: "Distinct prompts with at least one answer that day" }
    CompetitorRow:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        rawName: { type: string }
        globalRank: { type: integer, nullable: true }
        domain: { type: string }
        baseDomain: { type: string }
        isStarred: { type: boolean }
        isUserBrand: { type: boolean }
        totalResults: { type: integer, description: "Analyzed AI answers in the window (denominator of all rates)" }
        visibleResults: { type: integer, description: "Answers where the brand is visible: named in the text OR its domain cited as a source" }
        visibilityRate: { type: number, description: "Visibility — visibleResults / totalResults × 100 (dashboard Visibility column)" }
        visibilityChange: { type: number, nullable: true, description: "Visibility change vs. previous period, percentage points" }
        visibilityIsMentionOnly: { type: boolean, description: "True when the citation leg could not be resolved for this brand and visibilityRate equals the mention-based rate" }
        totalMentions: { type: integer, description: "Answers naming the brand in the text" }
        mentionRate: { type: number, description: "Mention Rate — totalMentions / totalResults × 100" }
        mentionRateChange: { type: number, nullable: true }
        mentionsChange: { type: integer, nullable: true }
        totalCitations: { type: integer, nullable: true, description: "Answers citing the brand's domain as a source; null when unresolved" }
        citationRate: { type: number, nullable: true, description: "Citation Rate — totalCitations / totalResults × 100; null when unresolved" }
        citationRateChange: { type: number, nullable: true }
        avgMentionOrder: { type: number, nullable: true, description: "Ordinal Position KPI — average rank among brands named (1 = named first)" }
        avgMentionOrderChange: { type: number, nullable: true }
        firstMentionSharePercent: { type: number, nullable: true }
        top3MentionSharePercent: { type: number, nullable: true }
        shareOfVoicePercent: { type: number, nullable: true }
        shareOfVoiceChange: { type: number, nullable: true }
        averagePosition: { type: number, nullable: true, description: "Mention depth (0 = top of the answer text); not a ranking" }
        positionChange: { type: number, nullable: true }
        sentimentScore: { type: number, nullable: true }
        sentimentChange: { type: number, nullable: true }
        promptCount: { type: integer }

paths:
  /me:
    get:
      operationId: getCurrentAccount
      summary: Get the account behind the API key
      description: Returns the authenticated account plus the key's scopes. Answers independently of whether the account owns any projects, which makes it the right endpoint for verifying a key and labelling a connection.
      tags: [Account]
      responses:
        "200":
          description: Account identity
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      email: { type: string, format: email }
                      accountName: { type: string }
                      tier: { type: string }
                      projectCount: { type: integer }
                      scopes:
                        type: array
                        items: { type: string, enum: [read, write, export] }
                      environment: { type: string, enum: [live, test] }
        "401":
          description: Missing, invalid or revoked API key

  /projects:
    get:
      operationId: listProjects
      summary: List projects
      tags: [Projects]
      responses:
        "200":
          description: List of projects
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string }
                        domain: { type: string }
                        websiteUrl: { type: string }
                        isDefault: { type: boolean }
                        language: { type: string, nullable: true, description: "Prompt language set at creation; null on older projects." }
                        models:
                          type: array
                          items: { type: string }
                          description: AI models the project's prompts run on (the dashboard's Model Settings; project setting, else account default). Valid values for the model filter.
                        isPitch: { type: boolean }
                        pitchDurationDays:
                          type: integer
                          enum: [1, 7, 14]
                          description: Only present on pitch projects.
                        pitchExpiredAt:
                          type: string
                          format: date-time
                          nullable: true
                          description: Only present on pitch projects; null while the pitch window is open.
    post:
      operationId: createProject
      summary: Create a project
      tags: [Projects]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, language]
              properties:
                name: { type: string }
                websiteUrl: { type: string, format: uri }
                domain: { type: string }
                description: { type: string }
                language:
                  type: string
                  description: Language for the brand analysis and generated prompts (case-insensitive).
                  enum: [en, de, fr, es, it, nl, pt, pl, sv, da, tr, ja, "no", fi, cs, ru, zh, zh-tw, ko, hi, ar, he]
                isPitch:
                  type: boolean
                  description: Create a pitch project (no project fee, max 50 prompts, auto-paused when the pitch window closes). Pitch slots are capped per agency package.
                pitchDurationDays:
                  type: integer
                  enum: [1, 7, 14]
                  default: 14
                  description: Pitch window in days. Ignored unless isPitch is true.
      responses:
        "200":
          description: Created project, including models (the account default until set via PUT /projects/{projectId}).
        "403":
          description: "Same gates as the dashboard: error.details.reason = agency_limit (details.limit = projects, no free client slot) or pitch_project_limit (all pitch slots in use). Pitch projects require an Agency account."

  /projects/{projectId}:
    get:
      operationId: getProject
      summary: Get project details
      tags: [Projects]
      parameters:
        - $ref: "#/components/parameters/projectId"
      responses:
        "200":
          description: Project details
    put:
      operationId: updateProject
      summary: Update a project
      tags: [Projects]
      parameters:
        - $ref: "#/components/parameters/projectId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                websiteUrl: { type: string }
                domain: { type: string }
                description: { type: string }
                convertFromPitch:
                  type: boolean
                  description: Set to true to convert a pitch project into a full client project. Checks a free client slot and the plan's answer budget, then reactivates paused prompts.
                models:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum: [chatgpt, perplexity, ai_overview, google_ai_mode, copilot, claude, gemini, grok, mistral, deepseek]
                  description: "AI models the project's prompts run on (API counterpart of the dashboard's Model Settings). Replaces the full set and re-syncs every prompt of the project (syncedPromptCount). Same guards as the dashboard: plan model cap, Brand prompt budget, Agency answer projection (meteredNotice on the top package)."
      responses:
        "200":
          description: "Updated. With models: models, syncedPromptCount and optionally meteredNotice."
        "403":
          description: "Limit violation; error.details.reason = model_limit | model_not_available | prompt_budget_exceeded | agency_limit."

  /projects/{projectId}/prompts:
    get:
      operationId: listPrompts
      summary: List prompts with metrics
      tags: [Prompts]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - $ref: "#/components/parameters/timeframe"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/language"
        - $ref: "#/components/parameters/tags"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/limit"
        - name: search
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Paginated prompt list with visibility metrics. Per prompt, isVisible = the brand was named in the answer text OR its own domain was cited in at least one answer; totalMentions/mentionRate = answers naming the brand (count / % of resultCount); ownDomainCitations/citationRate = answers citing the own domain or a domain alias (count / % of resultCount).
    post:
      operationId: addPrompt
      summary: Add a prompt to track
      description: Saves the prompt and queues it for processing immediately (enqueued=true in the response). The prompt inherits the project's domain, brand name, synonyms, domain aliases and tracking frequency; the project needs a websiteUrl, otherwise 400.
      tags: [Prompts]
      parameters:
        - $ref: "#/components/parameters/projectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [prompt]
              properties:
                prompt: { type: string, description: "The AI prompt to track" }
                models:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    enum: [chatgpt, perplexity, ai_overview, google_ai_mode, copilot, claude, gemini, grok, mistral, deepseek]
                  description: Optional. Defaults to the project's enabled models (Model Settings). Must be a subset of them, otherwise 400 with a message starting model_not_enabled. Enable models via PUT /projects/{projectId}.
                language: { type: string, description: "Language / market code (e.g. de, en). Defaults to the project language; normalised to the dashboard spelling (de -> DE)." }
                tags: { type: array, items: { type: string } }
      responses:
        "200":
          description: Created prompt
        "400":
          description: Validation error, e.g. model_not_enabled or project without websiteUrl.
        "403":
          description: "Same limits as the dashboard; error.details.reason = prompt_limit | prompt_budget_exceeded | agency_limit | pitch_prompt_limit | pitch_expired."

  /projects/{projectId}/metrics:
    get:
      operationId: getDailyMetrics
      summary: Get daily visibility KPIs
      description: Returns current vs previous period metrics — Visibility (answers where the brand is named in the text OR its own domain is cited as a source), Mention Rate (answers naming the brand), Citation Rate (answers citing the own domain), mentions, citations, position, and sentiment. Accepts either a relative timeframe or an absolute startDate/endDate range; the comparison period is always the same number of days immediately before the current period.
      tags: [Metrics]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - $ref: "#/components/parameters/timeframe"
        - $ref: "#/components/parameters/startDate"
        - $ref: "#/components/parameters/endDate"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/language"
        - $ref: "#/components/parameters/tags"
      responses:
        "200":
          description: Current period (`today`) and previous period (`yesterday`) aggregates with changes
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      today:
                        $ref: "#/components/schemas/PeriodMetrics"
                      yesterday:
                        $ref: "#/components/schemas/PeriodMetrics"
                      changes:
                        type: object
                        description: Current minus previous period, in the unit of the respective metric (percentage points for rates)
                        properties:
                          visibleChange: { type: integer }
                          visibilityRateChange: { type: number }
                          responseVisibilityRateChange: { type: number, description: "Visibility change in percentage points" }
                          mentionsChange: { type: integer }
                          citedChange: { type: integer }
                          mentionRateChange: { type: number }
                          citationRateChange: { type: number }
                          positionChange: { type: number }
                          sentimentChange: { type: number }

  /projects/{projectId}/metrics/timeseries:
    get:
      operationId: getTimeseries
      summary: Get time series chart data
      description: Returns daily data points for visibility (named OR cited), mentions, citations, ordinal position, mention depth, and sentiment.
      tags: [Metrics]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - $ref: "#/components/parameters/timeframe"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/language"
        - $ref: "#/components/parameters/tags"
        - $ref: "#/components/parameters/startDate"
        - $ref: "#/components/parameters/endDate"
      responses:
        "200":
          description: Daily data points under `data.chartData`
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      chartData:
                        type: array
                        items:
                          $ref: "#/components/schemas/TimeseriesDay"

  /projects/{projectId}/competitors:
    get:
      operationId: getCompetitorRanking
      summary: Get competitor visibility ranking
      description: Ranked competitors with Visibility (named OR cited), Mention Rate, Citation Rate, mentions, Share of Voice, ordinal position, mention depth, and sentiment.
      tags: [Competitors]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - $ref: "#/components/parameters/timeframe"
        - $ref: "#/components/parameters/startDate"
        - $ref: "#/components/parameters/endDate"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/language"
        - $ref: "#/components/parameters/tags"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/limit"
        - name: search
          in: query
          schema: { type: string }
        - name: sortBy
          in: query
          schema: { type: string, enum: [visibility, mentions, position, sentiment, name], default: visibility }
      responses:
        "200":
          description: Paginated competitor ranking
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      competitors:
                        type: array
                        items:
                          $ref: "#/components/schemas/CompetitorRow"
                      pagination:
                        type: object
                        properties:
                          page: { type: integer }
                          limit: { type: integer }
                          total: { type: integer }
                          hasMore: { type: boolean }

  /projects/{projectId}/sources:
    get:
      operationId: getSourceRanking
      summary: Get citation source ranking
      tags: [Sources]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - $ref: "#/components/parameters/timeframe"
        - $ref: "#/components/parameters/startDate"
        - $ref: "#/components/parameters/endDate"
        - $ref: "#/components/parameters/model"
        - $ref: "#/components/parameters/language"
        - $ref: "#/components/parameters/tags"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/limit"
        - name: search
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Paginated source ranking

  /projects/{projectId}/tags:
    get:
      operationId: listTags
      summary: List tags
      tags: [Tags]
      parameters:
        - $ref: "#/components/parameters/projectId"
      responses:
        "200":
          description: Tag list with counts
    post:
      operationId: createTag
      summary: Create and apply a tag
      tags: [Tags]
      parameters:
        - $ref: "#/components/parameters/projectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, promptIds]
              properties:
                name: { type: string }
                promptIds: { type: array, items: { type: string } }
      responses:
        "200":
          description: Tag created and applied

  /projects/{projectId}/export:
    get:
      operationId: exportData
      summary: Bulk export tracking data
      description: Export all prompts with full results data. Requires 'export' scope.
      tags: [Export]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - $ref: "#/components/parameters/timeframe"
        - $ref: "#/components/parameters/startDate"
        - $ref: "#/components/parameters/endDate"
      responses:
        "200":
          description: Full export with prompts and results

  /projects/{projectId}/attribution:
    get:
      operationId: listAttribution
      summary: List attribution responses
      description: Get paginated attribution survey responses with filters.
      tags: [Attribution]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - name: days
          in: query
          schema: { type: integer, default: 30 }
          description: "Number of days to look back"
        - name: category
          in: query
          schema: { type: string }
          description: "Filter by channel category (ai_search, organic_search, paid, social, referral, content, direct, other)"
        - name: source
          in: query
          schema: { type: string }
          description: "Filter by source (widget, hubspot, typeform, api, etc.)"
        - $ref: "#/components/parameters/page"
        - $ref: "#/components/parameters/limit"
      responses:
        "200":
          description: Paginated attribution responses
    post:
      operationId: createAttribution
      summary: Submit attribution response
      description: Record a self-reported attribution response. Requires write scope.
      tags: [Attribution]
      parameters:
        - $ref: "#/components/parameters/projectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [channelId]
              properties:
                channelId:
                  type: string
                  description: "e.g. ai_search, ai_chatgpt, google_search, social_media, paid_ads, referral, content, other"
                respondentEmail: { type: string }
                respondentName: { type: string }
                respondentExternalId: { type: string }
                dealValue: { type: number }
                dealCurrency: { type: string, default: "USD" }
                formId: { type: string }
                pageUrl: { type: string }
                metadata: { type: object }
      responses:
        "200":
          description: Attribution response created

  /projects/{projectId}/attribution/summary:
    get:
      operationId: getAttributionSummary
      summary: Attribution summary
      description: Aggregated data by channel, category, and source. Shows AI search percentage and total revenue.
      tags: [Attribution]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - name: days
          in: query
          schema: { type: integer, default: 30 }
      responses:
        "200":
          description: Attribution summary with channel breakdown

  /projects/{projectId}/attribution/bulk:
    post:
      operationId: bulkAttribution
      summary: Bulk import attribution responses
      description: Import up to 1000 attribution responses at once. Requires write scope.
      tags: [Attribution]
      parameters:
        - $ref: "#/components/parameters/projectId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [responses]
              properties:
                responses:
                  type: array
                  maxItems: 1000
                  items:
                    type: object
                    required: [channelId]
                    properties:
                      channelId: { type: string }
                      respondentEmail: { type: string }
                      respondentName: { type: string }
                      dealValue: { type: number }
                      dealCurrency: { type: string }
                      formId: { type: string }
                      pageUrl: { type: string }
      responses:
        "200":
          description: Bulk import result

  /projects/{projectId}/tasks:
    get:
      operationId: listTasks
      summary: List optimization tasks
      description: AI-generated optimization tasks for the project, sorted by priority score (impact weighted against effort). Read-only; status changes happen in the dashboard.
      tags: [Tasks]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - name: status
          in: query
          schema: { type: string, default: open }
          description: "Comma-separated statuses: open, in_progress, done, dismissed — or all"
        - name: category
          in: query
          schema:
            type: string
            enum: [technical, visibility, content, offsite, reputation, competitor, setup]
        - name: minImpact
          in: query
          schema: { type: integer, minimum: 1, maximum: 10 }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
      responses:
        "200":
          description: Task list with per-status counts
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      counts:
                        type: object
                        properties:
                          open: { type: integer }
                          in_progress: { type: integer }
                          done: { type: integer }
                          dismissed: { type: integer }
                      total: { type: integer }
                      page: { type: integer }
                      limit: { type: integer }
                      hasMore: { type: boolean }
                      tasks:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string, format: uuid }
                            title: { type: string }
                            category: { type: string }
                            status: { type: string }
                            impact: { type: integer }
                            effort: { type: integer }
                            priorityScore: { type: integer }
                            autoResolved: { type: boolean }
                            assigneeEmail: { type: string, nullable: true }
                            createdAt: { type: string, format: date-time }
                            updatedAt: { type: string, format: date-time }

  /projects/{projectId}/tasks/{taskId}:
    get:
      operationId: getTask
      summary: Get one task in full
      description: Full task detail including description, step-by-step plan, the evidence that triggered it, and the structured content plan for content tasks.
      tags: [Tasks]
      parameters:
        - $ref: "#/components/parameters/projectId"
        - name: taskId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Full task detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      title: { type: string }
                      category: { type: string }
                      status: { type: string }
                      impact: { type: integer }
                      effort: { type: integer }
                      priorityScore: { type: integer }
                      description: { type: string }
                      steps:
                        type: array
                        items: { type: string }
                      evidence:
                        type: object
                        properties:
                          summary: { type: string }
                          metrics:
                            type: array
                            items:
                              type: object
                              properties:
                                label: { type: string }
                                value: { type: string }
                          items:
                            type: array
                            items:
                              type: object
                              properties:
                                label: { type: string }
                                detail: { type: string }
                          deepLink: { type: string }
                      contentPlan:
                        type: object
                        nullable: true
                      autoResolvable: { type: boolean }
                      autoResolved: { type: boolean }
                      resolvedAt: { type: string, format: date-time, nullable: true }
                      lastSeenAt: { type: string, format: date-time }
                      createdAt: { type: string, format: date-time }
                      updatedAt: { type: string, format: date-time }
        "404":
          description: Task not found
