components:
  schemas:
    AcceptSuggestionBody:
      additionalProperties: false
      properties:
        tag_ids:
          anyOf:
          - items:
              maximum: 2147483647
              minimum: 1
              type: integer
            maxItems: 100
            type: array
          - type: 'null'
          description: Project-scoped tag IDs to attach to the new prompt. At most
            100, each 1-2147483647.
          title: Tag Ids
        text_override:
          anyOf:
          - maxLength: 300
            minLength: 10
            type: string
          - type: 'null'
          description: Optional rewrite of suggested_text before tracking. 10-300
            characters.
          title: Text Override
      title: AcceptSuggestionBody
      type: object
    AcceptSuggestionResponse:
      additionalProperties: true
      properties:
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of acceptance.
          title: Created At
        id_lite_query:
          description: Newly created tracked-prompt identifier.
          title: Id Lite Query
          type: integer
        id_prompt_suggestion:
          description: Source suggestion identifier (now marked accepted).
          title: Id Prompt Suggestion
          type: integer
        text:
          description: The tracked prompt's text after any override.
          title: Text
          type: string
      required:
      - id_lite_query
      - text
      - id_prompt_suggestion
      title: AcceptSuggestionResponse
      type: object
    AccuracyAnswerItem:
      description: One contradicting answer, with the words that contradict the fact.
      properties:
        check_date:
          description: UTC calendar day of the check, `YYYY-MM-DD`.
          title: Check Date
          type: string
        check_no:
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count.
          title: Check No
          type: integer
        checked_at:
          description: ISO-8601 UTC timestamp of the check.
          title: Checked At
          type: string
        claim_id:
          anyOf:
          - type: string
          - type: 'null'
          description: The claim this answer belongs to, or null when it has none.
          title: Claim Id
        claimed_value:
          anyOf:
          - type: string
          - type: 'null'
          description: The value this answer stated instead of the fact, for example
            `59 USD`. Null when no value could be read from the answer.
          title: Claimed Value
        engine:
          description: Public key of the engine that answered.
          title: Engine
          type: string
        fact_version:
          description: The fact version the answer was judged against.
          title: Fact Version
          type: integer
        prompt_id:
          description: Identifier of the prompt that was asked.
          title: Prompt Id
          type: integer
        prompt_text:
          description: The prompt's text.
          title: Prompt Text
          type: string
        quote:
          description: The engine's own words, quoted from the answer.
          title: Quote
          type: string
      required:
      - prompt_id
      - prompt_text
      - engine
      - check_no
      - check_date
      - checked_at
      - quote
      - fact_version
      - claim_id
      - claimed_value
      title: AccuracyAnswerItem
      type: object
    AccuracyAnswersResponse:
      description: Contradicting answers of one problem, offset paged.
      properties:
        data:
          description: Answers on this page.
          items:
            $ref: '#/components/schemas/AccuracyAnswerItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Answers matching the filters.
          title: Total
          type: integer
      required:
      - data
      - total
      - limit
      - offset
      title: AccuracyAnswersResponse
      type: object
    AccuracyClaim:
      description: What the answers claimed instead of the fact, in the engines' own
        words.
      properties:
        answer_count:
          description: Answers that made the claim.
          title: Answer Count
          type: integer
        claim_id:
          anyOf:
          - type: string
          - type: 'null'
          description: Identifier of the claim within the problem. Use it to filter
            the answers. Null when the claim has no identifier.
          title: Claim Id
        claimed_value:
          anyOf:
          - type: string
          - type: 'null'
          description: The value the engines stated instead of the fact, as the newest
            answer of the claim gave it, for example `59 USD`. Null when no value
            could be read from it.
          title: Claimed Value
        engines:
          description: Public keys of the engines that made the claim.
          items:
            type: string
          title: Engines
          type: array
        last_seen:
          description: ISO-8601 UTC timestamp of the latest such answer's check.
          title: Last Seen
          type: string
        quote:
          description: The engine's own words, quoted from an answer.
          title: Quote
          type: string
      required:
      - claim_id
      - quote
      - engines
      - answer_count
      - last_seen
      - claimed_value
      title: AccuracyClaim
      type: object
    AccuracyEngineCount:
      description: What one engine's answers said about the fact.
      properties:
        contradicts:
          description: Answers that contradict the fact.
          title: Contradicts
          type: integer
        engine:
          description: Public engine key.
          title: Engine
          type: string
        supports:
          description: Answers that state the fact.
          title: Supports
          type: integer
        unclear:
          description: Answers that address the fact without settling it.
          title: Unclear
          type: integer
      required:
      - engine
      - contradicts
      - supports
      - unclear
      title: AccuracyEngineCount
      type: object
    AccuracyFactCurrent:
      description: The fact as it reads now, when it differs from the version that
        was judged.
      properties:
        category:
          description: The current category.
          title: Category
          type: string
        statement:
          description: The current statement.
          title: Statement
          type: string
        version:
          description: The current version.
          title: Version
          type: integer
      required:
      - statement
      - category
      - version
      title: AccuracyFactCurrent
      type: object
    AccuracyHistoryEvent:
      description: One event in a problem's history.
      properties:
        at:
          description: ISO-8601 UTC timestamp of the event.
          title: At
          type: string
        detail:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          description: Facts about the event, such as the engines or the versions.
          title: Detail
        kind:
          description: What happened.
          enum:
          - opened
          - reopened
          - resolved
          - dismissed
          - undismissed
          - fact_changed
          title: Kind
          type: string
      required:
      - kind
      - at
      title: AccuracyHistoryEvent
      type: object
    AccuracyNotCheckedByReason:
      description: Answers that were not checked in the window, by why.
      properties:
        answer_missing:
          description: The answer text was no longer available.
          title: Answer Missing
          type: integer
        invalid_output:
          description: The check ran but its result could not be used.
          title: Invalid Output
          type: integer
        judge_failed:
          description: The check could not run.
          title: Judge Failed
          type: integer
        spend_cap:
          description: Skipped because the project's check allowance was spent.
          title: Spend Cap
          type: integer
      required:
      - spend_cap
      - judge_failed
      - invalid_output
      - answer_missing
      title: AccuracyNotCheckedByReason
      type: object
    AccuracyProblemDetailResponse:
      description: 'A problem in full: the fact as judged and now, every claim, per-engine
        counts, sources, history.'
      properties:
        answer_count:
          description: Contradicting answers in the problem's current episode.
          title: Answer Count
          type: integer
        claims:
          description: Every claim, newest first.
          items:
            $ref: '#/components/schemas/AccuracyClaim'
          title: Claims
          type: array
        dismiss_reason:
          anyOf:
          - enum:
            - not_wrong
            - harmless
            - other
            type: string
          - type: 'null'
          description: Why a dismissed problem was dismissed, or null.
          title: Dismiss Reason
        dismissed_until:
          anyOf:
          - type: string
          - type: 'null'
          description: End of the 30-day hold after a dismissal, or null.
          title: Dismissed Until
        engine_counts:
          description: Per-engine counts of contradicting, supporting and unclear
            answers.
          items:
            $ref: '#/components/schemas/AccuracyEngineCount'
          title: Engine Counts
          type: array
        engines:
          description: Public keys of the engines with contradicting answers in the
            problem's episode. On a resolved problem whose resolution is `now_correct`,
            the engines whose latest answer states the fact.
          items:
            type: string
          title: Engines
          type: array
        fact:
          $ref: '#/components/schemas/AccuracyProblemFact'
          description: The fact the problem is about.
        fact_current:
          anyOf:
          - $ref: '#/components/schemas/AccuracyFactCurrent'
          - type: 'null'
          description: The fact as it reads now. Null when it equals the version that
            was judged.
        history:
          description: Every event, oldest first.
          items:
            $ref: '#/components/schemas/AccuracyHistoryEvent'
          title: History
          type: array
        id:
          description: Identifier of the problem.
          title: Id
          type: string
        last_contradiction_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the latest contradicting answer's
            check. Null when no contradicting answer is recorded for the problem.
          title: Last Contradiction At
        opened_at:
          description: ISO-8601 UTC timestamp the problem opened.
          title: Opened At
          type: string
        outside_filter:
          description: 'True when an `llms` filter is set and the problem has no figures
            inside it: its contradicting answers (or, on a resolved `now_correct`
            problem, its answers stating the fact) are all on tracked engines outside
            the filter. The figures are then empty.'
          title: Outside Filter
          type: boolean
        resolution:
          anyOf:
          - enum:
            - now_correct
            - no_longer_stated
            - no_longer_checked
            type: string
          - type: 'null'
          description: How a resolved problem resolved, or null.
          title: Resolution
        resolution_cause:
          anyOf:
          - enum:
            - engine_left_plan
            - prompts_removed
            type: string
          - type: 'null'
          description: 'Why a problem resolved as `no_longer_checked`: `engine_left_plan`
            when the engines that contradicted the fact are no longer in the plan,
            `prompts_removed` when the prompts that drew those answers were removed.
            Null for any other resolution and for problems that are not resolved.'
          title: Resolution Cause
        resolved_at:
          anyOf:
          - type: string
          - type: 'null'
          description: When the problem resolved, or null.
          title: Resolved At
        since_dismissal_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Contradicting answers checked since the dismissal. Set only
            while the problem is dismissed, null otherwise.
          title: Since Dismissal Count
        sources:
          $ref: '#/components/schemas/AccuracySources'
          description: Pages cited in the contradicting answers.
        status:
          description: Where the problem stands.
          enum:
          - open
          - resolved
          - dismissed
          - fact_changed
          title: Status
          type: string
      required:
      - id
      - fact
      - status
      - resolution
      - resolution_cause
      - dismiss_reason
      - opened_at
      - last_contradiction_at
      - resolved_at
      - dismissed_until
      - answer_count
      - since_dismissal_count
      - engines
      - claims
      - outside_filter
      - fact_current
      - engine_counts
      - sources
      - history
      title: AccuracyProblemDetailResponse
      type: object
    AccuracyProblemFact:
      description: The fact a problem is about, as it was judged.
      properties:
        category:
          description: The fact's category at that version.
          title: Category
          type: string
        id:
          description: Identifier of the fact.
          title: Id
          type: string
        statement:
          description: The statement at the version that was judged.
          title: Statement
          type: string
        status:
          description: '`active`, or `paused` when the fact is not being checked.'
          enum:
          - active
          - paused
          title: Status
          type: string
        version:
          description: The fact version that was judged.
          title: Version
          type: integer
      required:
      - id
      - statement
      - category
      - version
      - status
      title: AccuracyProblemFact
      type: object
    AccuracyProblemItem:
      description: 'One problem: a fact that answers contradict. Counts only, never
        a rate.'
      properties:
        answer_count:
          description: Contradicting answers in the problem's current episode.
          title: Answer Count
          type: integer
        claims:
          description: Up to three claims, newest first.
          items:
            $ref: '#/components/schemas/AccuracyClaim'
          title: Claims
          type: array
        dismiss_reason:
          anyOf:
          - enum:
            - not_wrong
            - harmless
            - other
            type: string
          - type: 'null'
          description: Why a dismissed problem was dismissed, or null.
          title: Dismiss Reason
        dismissed_until:
          anyOf:
          - type: string
          - type: 'null'
          description: End of the 30-day hold after a dismissal, or null.
          title: Dismissed Until
        engines:
          description: Public keys of the engines with contradicting answers in the
            problem's episode. On a resolved problem whose resolution is `now_correct`,
            the engines whose latest answer states the fact.
          items:
            type: string
          title: Engines
          type: array
        fact:
          $ref: '#/components/schemas/AccuracyProblemFact'
          description: The fact the problem is about.
        id:
          description: Identifier of the problem.
          title: Id
          type: string
        last_contradiction_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the latest contradicting answer's
            check. Null when no contradicting answer is recorded for the problem.
          title: Last Contradiction At
        opened_at:
          description: ISO-8601 UTC timestamp the problem opened.
          title: Opened At
          type: string
        outside_filter:
          description: 'True when an `llms` filter is set and the problem has no figures
            inside it: its contradicting answers (or, on a resolved `now_correct`
            problem, its answers stating the fact) are all on tracked engines outside
            the filter. The figures are then empty.'
          title: Outside Filter
          type: boolean
        resolution:
          anyOf:
          - enum:
            - now_correct
            - no_longer_stated
            - no_longer_checked
            type: string
          - type: 'null'
          description: How a resolved problem resolved, or null.
          title: Resolution
        resolution_cause:
          anyOf:
          - enum:
            - engine_left_plan
            - prompts_removed
            type: string
          - type: 'null'
          description: 'Why a problem resolved as `no_longer_checked`: `engine_left_plan`
            when the engines that contradicted the fact are no longer in the plan,
            `prompts_removed` when the prompts that drew those answers were removed.
            Null for any other resolution and for problems that are not resolved.'
          title: Resolution Cause
        resolved_at:
          anyOf:
          - type: string
          - type: 'null'
          description: When the problem resolved, or null.
          title: Resolved At
        since_dismissal_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Contradicting answers checked since the dismissal. Set only
            while the problem is dismissed, null otherwise.
          title: Since Dismissal Count
        status:
          description: Where the problem stands.
          enum:
          - open
          - resolved
          - dismissed
          - fact_changed
          title: Status
          type: string
      required:
      - id
      - fact
      - status
      - resolution
      - resolution_cause
      - dismiss_reason
      - opened_at
      - last_contradiction_at
      - resolved_at
      - dismissed_until
      - answer_count
      - since_dismissal_count
      - engines
      - claims
      - outside_filter
      title: AccuracyProblemItem
      type: object
    AccuracyProblemListResponse:
      description: Problems, offset paged.
      properties:
        data:
          description: Problems on this page.
          items:
            $ref: '#/components/schemas/AccuracyProblemItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Problems matching the filters.
          title: Total
          type: integer
      required:
      - data
      - total
      - limit
      - offset
      title: AccuracyProblemListResponse
      type: object
    AccuracySourceCount:
      description: A page that contradicting answers cited.
      properties:
        cited_in:
          description: Contradicting answers that cited it.
          title: Cited In
          type: integer
        url:
          description: The cited URL.
          title: Url
          type: string
      required:
      - url
      - cited_in
      title: AccuracySourceCount
      type: object
    AccuracySources:
      description: Pages cited in the contradicting answers. A citation is not a cause.
      properties:
        answers_total:
          description: Contradicting answers counted.
          title: Answers Total
          type: integer
        answers_without_sources:
          description: Contradicting answers that cited nothing.
          title: Answers Without Sources
          type: integer
        other:
          description: Pages on other sites.
          items:
            $ref: '#/components/schemas/AccuracySourceCount'
          title: Other
          type: array
        own:
          description: Pages on the project's own domains.
          items:
            $ref: '#/components/schemas/AccuracySourceCount'
          title: Own
          type: array
      required:
      - own
      - other
      - answers_total
      - answers_without_sources
      title: AccuracySources
      type: object
    AddCompetitorTagMembersBody:
      additionalProperties: false
      properties:
        members:
          description: Brands and domains to add to the tag. Adding a hidden brand
            or domain unhides it.
          items:
            $ref: '#/components/schemas/CompetitorTagMemberInput'
          maxItems: 100
          minItems: 1
          title: Members
          type: array
      required:
      - members
      title: AddCompetitorTagMembersBody
      type: object
    AnswerAccuracySummaryResponse:
      description: Where answer accuracy stands for the project, as counts over the
        last 30 days.
      properties:
        active_facts_count:
          description: Facts that are active.
          title: Active Facts Count
          type: integer
        checked_count:
          description: Answers checked against a fact in the window.
          title: Checked Count
          type: integer
        facts_count:
          description: Facts registered.
          title: Facts Count
          type: integer
        last_checked_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the latest check, or null when none
            ran.
          title: Last Checked At
        not_checked_by_reason:
          $ref: '#/components/schemas/AccuracyNotCheckedByReason'
          description: '`not_checked_count` split by reason.'
        not_checked_count:
          description: Answers that were not checked in the window. Never counted
            as correct.
          title: Not Checked Count
          type: integer
        not_mentioned_count:
          description: Answers that did not mention the brand and so were not judged.
          title: Not Mentioned Count
          type: integer
        open_count:
          description: Open problems whose fact is active. Problems on a paused fact
            are in `paused_open_count`.
          title: Open Count
          type: integer
        paused_open_count:
          description: Open problems whose fact is paused. They are frozen (not checked)
            and are not in `open_count`.
          title: Paused Open Count
          type: integer
        state:
          description: '`no_facts`: no facts registered. `facts_paused`: facts exist
            and none is active, so nothing is checked. `waiting`: active facts and
            nothing checked in the window yet. `no_contradictions`: at least one answer
            was checked in the window and no problem is open (dismissed problems may
            still be collecting contradictions). `problems`: at least one open problem.
            A model makes the judgement and can be wrong: dismiss a problem that is
            not one, or edit the fact.'
          enum:
          - no_facts
          - facts_paused
          - waiting
          - no_contradictions
          - problems
          title: State
          type: string
        window_days:
          description: Length of the window the counts cover, in days.
          title: Window Days
          type: integer
      required:
      - state
      - open_count
      - paused_open_count
      - facts_count
      - active_facts_count
      - window_days
      - checked_count
      - not_mentioned_count
      - not_checked_count
      - not_checked_by_reason
      - last_checked_at
      title: AnswerAccuracySummaryResponse
      type: object
    AppliedFilters:
      additionalProperties: false
      description: 'The window and engines a response''s numbers were computed over.


        Built only by ``deps.applied_block`` (``src/routers/v1/deps.py``), which

        maps each engine to its public name, so no internal engine key can reach a

        consumer through it. Closed to extra keys: everything it carries is

        declared here.'
      properties:
        date_from:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: First day read, `YYYY-MM-DD` (UTC). Null only on `checks/answers`
            in `latest` or `check_no` mode, which read no window.
          title: Date From
        date_to:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: Last day read, `YYYY-MM-DD` (UTC), after a `date_to` later
            than today is clamped to today. Null exactly when `date_from` is.
          title: Date To
        engines:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: 'Public engine names the numbers cover: the requested `engines`
            the workspace''s plan includes, or the plan''s engines when `engines`
            is omitted. An engine you asked for that the plan lacks is not here, which
            is how a silently narrowed filter shows. Null where the read has no per-engine
            data (Search Console, the organic SERP rankings).'
          title: Engines
      required:
      - date_from
      - date_to
      - engines
      title: AppliedFilters
      type: object
    BrandAliasGroup:
      additionalProperties: true
      description: 'One canonical brand name and the variants merged into it.


        Brand mentions are reported under a canonical name, so two spellings of one

        brand are counted together rather than competing with each other. This is

        the map that decides those merges for names your workspace has configured.'
      properties:
        canonical_name:
          description: The name mentions are reported under once the merge is applied.
          title: Canonical Name
          type: string
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp the earliest variant in the group was
            added.
          title: Created At
        variants:
          description: Normalised fingerprint of each variant merged into `canonical_name`.
            These are lowercased and whitespace-collapsed forms, not the original
            casing, which is not retained.
          items:
            type: string
          title: Variants
          type: array
      required:
      - canonical_name
      - variants
      title: BrandAliasGroup
      type: object
    BrandAliasListResponse:
      additionalProperties: true
      properties:
        data:
          description: Alias groups on this page.
          items:
            $ref: '#/components/schemas/BrandAliasGroup'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: BrandAliasListResponse
      type: object
    BrandFactCreateResponse:
      description: The created fact.
      properties:
        category:
          description: What the fact is about.
          enum:
          - price
          - plan
          - feature
          - location
          - other
          title: Category
          type: string
        created_at:
          description: ISO-8601 UTC timestamp the fact was created.
          title: Created At
          type: string
        id:
          description: Identifier of the fact.
          title: Id
          type: string
        problem_status:
          anyOf:
          - enum:
            - open
            - resolved
            - dismissed
            - fact_changed
            type: string
          - type: 'null'
          description: Status of the fact's problem, or null when it has none.
          title: Problem Status
        source:
          description: 'Where the fact was entered: the project wizard, the settings
            page or the API (`settings`), or a suggestion that was confirmed.'
          enum:
          - wizard
          - settings
          - suggested
          title: Source
          type: string
        statement:
          description: The fact, as a sentence about the business.
          title: Statement
          type: string
        status:
          description: '`active` facts are checked against new answers; `paused` facts
            are not.'
          enum:
          - active
          - paused
          title: Status
          type: string
        updated_at:
          description: ISO-8601 UTC timestamp of the last change.
          title: Updated At
          type: string
        version:
          description: Starts at 1 and rises each time `statement` or `category` changes.
          title: Version
          type: integer
      required:
      - id
      - statement
      - category
      - status
      - version
      - source
      - created_at
      - updated_at
      - problem_status
      title: BrandFactCreateResponse
      type: object
    BrandFactItem:
      description: 'One brand fact: a statement about the business the project''s
        answers are checked against.'
      properties:
        category:
          description: What the fact is about.
          enum:
          - price
          - plan
          - feature
          - location
          - other
          title: Category
          type: string
        created_at:
          description: ISO-8601 UTC timestamp the fact was created.
          title: Created At
          type: string
        id:
          description: Identifier of the fact.
          title: Id
          type: string
        problem_status:
          anyOf:
          - enum:
            - open
            - resolved
            - dismissed
            - fact_changed
            type: string
          - type: 'null'
          description: Status of the fact's problem, or null when it has none.
          title: Problem Status
        source:
          description: 'Where the fact was entered: the project wizard, the settings
            page or the API (`settings`), or a suggestion that was confirmed.'
          enum:
          - wizard
          - settings
          - suggested
          title: Source
          type: string
        statement:
          description: The fact, as a sentence about the business.
          title: Statement
          type: string
        status:
          description: '`active` facts are checked against new answers; `paused` facts
            are not.'
          enum:
          - active
          - paused
          title: Status
          type: string
        updated_at:
          description: ISO-8601 UTC timestamp of the last change.
          title: Updated At
          type: string
        version:
          description: Starts at 1 and rises each time `statement` or `category` changes.
          title: Version
          type: integer
      required:
      - id
      - statement
      - category
      - status
      - version
      - source
      - created_at
      - updated_at
      - problem_status
      title: BrandFactItem
      type: object
    BrandFactListResponse:
      description: The project's brand facts, offset paged.
      properties:
        data:
          description: Facts on this page.
          items:
            $ref: '#/components/schemas/BrandFactItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        max_facts:
          description: The most facts one project can hold.
          title: Max Facts
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Facts matching the filters.
          title: Total
          type: integer
      required:
      - data
      - total
      - limit
      - offset
      - max_facts
      title: BrandFactListResponse
      type: object
    BrandFactResp:
      properties:
        detail:
          title: Detail
          type: string
        sentiment:
          anyOf:
          - type: string
          - type: 'null'
          title: Sentiment
      required:
      - detail
      title: BrandFactResp
      type: object
    BrandFactUpdateResponse:
      description: The fact as stored after the update.
      properties:
        category:
          description: What the fact is about.
          enum:
          - price
          - plan
          - feature
          - location
          - other
          title: Category
          type: string
        created_at:
          description: ISO-8601 UTC timestamp the fact was created.
          title: Created At
          type: string
        id:
          description: Identifier of the fact.
          title: Id
          type: string
        problem_status:
          anyOf:
          - enum:
            - open
            - resolved
            - dismissed
            - fact_changed
            type: string
          - type: 'null'
          description: Status of the fact's problem, or null when it has none.
          title: Problem Status
        source:
          description: 'Where the fact was entered: the project wizard, the settings
            page or the API (`settings`), or a suggestion that was confirmed.'
          enum:
          - wizard
          - settings
          - suggested
          title: Source
          type: string
        statement:
          description: The fact, as a sentence about the business.
          title: Statement
          type: string
        status:
          description: '`active` facts are checked against new answers; `paused` facts
            are not.'
          enum:
          - active
          - paused
          title: Status
          type: string
        updated_at:
          description: ISO-8601 UTC timestamp of the last change.
          title: Updated At
          type: string
        version:
          description: Starts at 1 and rises each time `statement` or `category` changes.
          title: Version
          type: integer
      required:
      - id
      - statement
      - category
      - status
      - version
      - source
      - created_at
      - updated_at
      - problem_status
      title: BrandFactUpdateResponse
      type: object
    BrandResp:
      properties:
        brand:
          title: Brand
          type: string
        details:
          default: []
          items:
            $ref: '#/components/schemas/BrandFactResp'
          title: Details
          type: array
        order_of_mention:
          anyOf:
          - type: integer
          - type: 'null'
          title: Order Of Mention
        prominence:
          anyOf:
          - type: string
          - type: 'null'
          title: Prominence
        sentiment:
          anyOf:
          - type: string
          - type: 'null'
          title: Sentiment
      required:
      - brand
      title: BrandResp
      type: object
    CheckAnswerEngineResult:
      description: 'One engine''s full answer for a (prompt, check) pair.


        A plain `BaseModel`, not `_AllowExtra`: an upstream field is published only

        once it is declared here. That keeps the engine''s raw failure text, which
        can

        name a provider, off this surface, along with any key upstream adds later.

        A failed run is published as `failed: true` with no answer fields instead.'
      properties:
        brand_mention:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The answer named one of the project's tracked brands. Null
            when the check failed.
          title: Brand Mention
        brands:
          description: Every brand the answer named, in order of mention, each with
            the discrete facts the answer stated about it.
          items:
            $ref: '#/components/schemas/BrandResp'
          title: Brands
          type: array
        citations:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The answer cited one of the project's tracked URLs. Null when
            the check failed.
          title: Citations
        failed:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The engine's run of this check failed and returned no answer.
            When true, `response_text` is empty, `response_length` is 0, and `brand_mention`,
            `citations`, `sentiment` and `response_status` are null. A failed check
            is left out of `success_score` and of every rate. The failure reason is
            not published here; the check detail endpoint carries it as `error_message`.
          title: Failed
        favored_brand:
          anyOf:
          - type: string
          - type: 'null'
          description: The brand the answer favoured, or null when it favoured none.
          title: Favored Brand
        followup_queries:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Follow-up questions the engine suggested, or null when it offered
            none.
          title: Followup Queries
        response_length:
          description: Length of `response_text` in characters.
          title: Response Length
          type: integer
        response_status:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether the search returned an AI answer at all. Set on `google_aio`
            and `bing_ai_overview`, where a search can show no overview; null on the
            other engines, and null when the check failed, which is not the same as
            no overview.
          title: Response Status
        response_text:
          description: The engine's full answer as markdown, untruncated. Empty when
            the engine returned no text or its check failed.
          title: Response Text
          type: string
        sentiment:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Sentiment of the answer toward your brand: `Positive`, `Mixed`,
            `Negative` or `Unknown`. Null when the check failed.'
          title: Sentiment
        sources:
          description: The sources the engine cited, in citation order.
          items:
            $ref: '#/components/schemas/CheckSourceResp'
          title: Sources
          type: array
        subtopics:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Subtopics the answer covered, or null when none were extracted.
          title: Subtopics
      required:
      - response_text
      - brand_mention
      - citations
      - sentiment
      - favored_brand
      - response_length
      - subtopics
      - followup_queries
      - brands
      - sources
      - response_status
      title: CheckAnswerEngineResult
      type: object
    CheckAnswersItem:
      description: One (prompt, check) pair with every requested engine's full answer.
      properties:
        answered_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engines whose run of this check answered, counted over the
            engines `success_score` averages, `bing_ai_overview` included. A `google_aio`
            or `bing_ai_overview` run that showed no overview neither answered nor
            failed. 0 when none answered, in which case `success_score` is null. Null
            when the count is not reported.
          title: Answered Checks
        bing_organic_position:
          anyOf:
          - type: integer
          - type: 'null'
          description: Best Bing organic rank (1 is the top result) a URL from the
            project's domain held at this check. Bing organic results are captured
            by `bing_ai_overview` checks. `null` when the domain did not rank in the
            top results, the check has no Bing organic data, or `engines` leaves out
            `bing_ai_overview`.
          title: Bing Organic Position
        check_no:
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count.
          title: Check No
          type: integer
        engines:
          additionalProperties:
            anyOf:
            - $ref: '#/components/schemas/CheckAnswerEngineResult'
            - type: 'null'
          description: Per-engine answer, keyed by public engine name. Every requested
            engine on the workspace's plan is present; null means that engine has
            no row for this pair, and an engine whose check failed is present with
            `failed` true.
          title: Engines
          type: object
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engines whose run of this check failed, counted over the same
            engines as `answered_checks`. A failed run returned no answer, so it is
            left out of `success_score` rather than scored as 0. Null when the count
            is not reported.
          title: Failed Checks
        google_organic_position:
          anyOf:
          - type: integer
          - type: 'null'
          description: Best Google organic rank (1 is the top result) a URL from the
            project's domain held at this check. Google organic results are captured
            by `google_aio` checks. `null` when the domain did not rank in the top
            results, the check has no Google organic data, or `engines` leaves out
            `google_aio`.
          title: Google Organic Position
        prompt_id:
          description: Identifier of the prompt.
          title: Prompt Id
          type: integer
        prompt_text:
          description: The prompt's tracked query text.
          title: Prompt Text
          type: string
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Cross-engine score for this check over the returned engines
            (0-1). Failed checks are left out, and so is a `google_aio` or `bing_ai_overview`
            run that showed no overview. When no engine answered this check, `success_score`
            is null and `answered_checks` is 0. `answered_checks` and `failed_checks`
            count over the same engines this score averages.
          title: Success Score
        tags:
          description: Tags on the prompt, sorted by name case-insensitively; empty
            for an untagged prompt.
          items:
            $ref: '#/components/schemas/PromptTag'
          title: Tags
          type: array
        timestamp:
          description: ISO-8601 UTC timestamp of the earliest engine answer in this
            pair.
          title: Timestamp
          type: string
      required:
      - prompt_id
      - prompt_text
      - tags
      - check_no
      - timestamp
      - success_score
      - engines
      title: CheckAnswersItem
      type: object
    CheckAnswersListResponse:
      description: One cursor page of full engine answers, with no total.
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: (prompt, check) pairs on this page, in `(prompt_id, check_no)`
            order. A page can hold fewer than `limit` items, or none, while `next_cursor`
            is not null.
          items:
            $ref: '#/components/schemas/CheckAnswersItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        next_cursor:
          anyOf:
          - type: string
          - type: 'null'
          description: Pass back as `cursor` to read the next page. Null when the
            walk is complete.
          title: Next Cursor
      required:
      - applied
      - data
      - next_cursor
      - limit
      title: CheckAnswersListResponse
      type: object
    CheckDetailResp:
      properties:
        check_no:
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count.
          title: Check No
          type: integer
        competitor_brands:
          default: []
          description: Brands carrying at least one competitor tag.
          items:
            type: string
          title: Competitor Brands
          type: array
        engines:
          additionalProperties:
            anyOf:
            - $ref: '#/components/schemas/EngineResultResp'
            - type: 'null'
          title: Engines
          type: object
        timestamp:
          description: ISO-8601 UTC timestamp the check was recorded.
          title: Timestamp
          type: string
        user_brand:
          anyOf:
          - type: string
          - type: 'null'
          description: The project's own brand, as currently configured.
          title: User Brand
      required:
      - check_no
      - timestamp
      - engines
      title: CheckDetailResp
      type: object
    CheckEngineOutcome:
      additionalProperties: true
      description: 'The two booleans the check LISTING reports per engine.


        Deliberately narrower than the check DETAIL response, which carries the

        answer text, sentiment, named brands and cited sources. The listing exists

        to be paged cheaply across a prompt''s history, so it answers only "did this

        engine cite us" and "did it mention us"; fetch the detail endpoint for the

        evidence behind either.'
      properties:
        cited:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The engine's answer cited one of the project's tracked URLs.
            Null when the engine's check failed.
          title: Cited
        failed:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The engine's run of this check failed and returned no answer.
            A failed check is left out of `success_score` and of every rate. Null
            when not reported.
          title: Failed
        mentioned:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The engine's answer named one of the project's tracked brands.
            Null when the engine's check failed.
          title: Mentioned
      title: CheckEngineOutcome
      type: object
    CheckLatestResponse:
      description: 'The most recent check for a prompt, in the check-detail shape.


        Engines with no row for the check are omitted rather than null. Also

        carries four deprecated fields kept for /v1 compatibility: `success_score`,

        per-engine `cited` and `mentioned`, and the top-level `bing_ai_overview`.'
      properties:
        bing_ai_overview:
          anyOf:
          - $ref: '#/components/schemas/LatestCheckBingAiOverviewBlock'
          - type: 'null'
          deprecated: true
          description: Deprecated. Duplicates `engines.bing_ai_overview`, the same
            Bing AI Overview answer in the per-engine shape every other engine uses;
            read that instead. Null when the check has no Bing AI Overview answer.
        check_no:
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count.
          title: Check No
          type: integer
        competitor_brands:
          default: []
          description: Brands carrying at least one competitor tag.
          items:
            type: string
          title: Competitor Brands
          type: array
        engines:
          additionalProperties:
            $ref: '#/components/schemas/LatestCheckEngineResultResp'
          description: Per-engine result for this check, keyed by public engine name.
            Only engines with a row for the check appear; an engine that did not answer
            is omitted rather than null.
          title: Engines
          type: object
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          deprecated: true
          description: Deprecated. Always null on this endpoint, which never computed
            a cross-engine score. Read it from the first row of `GET /v1/projects/{project_id}/prompts/{prompt_id}/checks`
            (`limit=1`), which is the latest check.
          title: Success Score
        timestamp:
          description: ISO-8601 UTC timestamp the check was recorded.
          title: Timestamp
          type: string
        user_brand:
          anyOf:
          - type: string
          - type: 'null'
          description: The project's own brand, as currently configured.
          title: User Brand
      required:
      - check_no
      - timestamp
      - engines
      title: CheckLatestResponse
      type: object
    CheckListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: Checks on this page.
          items:
            $ref: '#/components/schemas/CheckSummary'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      title: CheckListResponse
      type: object
    CheckSourceResp:
      description: 'One source URL an engine drew from, for a single check.


        Typed rather than left as a bare dict because this is the evidence layer of

        the whole product - the actual pages an engine cited - and a consumer

        cannot code against a field list it has to discover from a live response.

        The shape is fixed upstream by ziptie-lite''s ``SourceRow``

        (``routers/query/models/prompt_detail_models.py``), which is a strict model,

        so these five keys are the whole contract rather than a sample of it.'
      properties:
        domain:
          description: Host of the cited URL, for grouping without parsing. On a `shopping`
            row this is Google's host rather than the merchant's; see `source_kind`.
          title: Domain
          type: string
        position:
          description: 1-based rank of this source within the engine's citation list
            for this check.
          title: Position
          type: integer
        source_kind:
          default: web
          description: 'Which citation surface this row came from. `web` is an ordinary
            cited page. `shopping` is a Google Shopping product panel, which is not
            a link to a merchant and does not redirect to one, so its domain is always
            Google''s - read the title for the product and the brand, never the domain.
            Panels are included in every count on this API; nothing here excludes
            them by default. Treat the value set as open: a kind you do not recognise
            is a surface added after your client was written, not an error.'
          title: Source Kind
          type: string
        source_title:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Title the engine gave the cited item. On a `shopping` row
            this is the product panel''s name, and the only brand signal the row carries,
            because the domain is Google''s. Null wherever no title reached the row,
            which is the norm rather than the anomaly: `chatgpt`, `copilot` and `perplexity`
            cite without one, so every row from those three is null, and every row
            analysed before titles were captured is null on all seven engines and
            cannot be backfilled. Expect null across the whole historical window,
            and read it as unknown rather than untitled.'
          title: Source Title
        url:
          description: The cited page URL, normalised.
          title: Url
          type: string
      required:
      - position
      - domain
      - url
      title: CheckSourceResp
      type: object
    CheckSummary:
      additionalProperties: true
      properties:
        answered_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engines whose run of this check answered, counted over the
            engines `success_score` averages, `bing_ai_overview` included. A `google_aio`
            or `bing_ai_overview` run that showed no overview neither answered nor
            failed. 0 when none answered, in which case `success_score` is null. Null
            when the count is not reported.
          title: Answered Checks
        check_no:
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count.
          title: Check No
          type: integer
        engines:
          anyOf:
          - additionalProperties:
              $ref: '#/components/schemas/CheckEngineOutcome'
            type: object
          - type: 'null'
          description: Per-engine outcome for this check, keyed by public engine name.
            Only engines with a row for the check appear, a failed one included with
            `failed` true. See the check detail endpoint for the answer text, sentiment,
            brands and cited sources behind these flags.
          title: Engines
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engines whose run of this check failed, counted over the same
            engines as `answered_checks`. A failed run returned no answer, so it is
            left out of `success_score` rather than scored as 0. Null when the count
            is not reported.
          title: Failed Checks
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Cross-engine score for this check (0-1). Failed checks are
            left out, and so is a `google_aio` or `bing_ai_overview` run that showed
            no overview. When no engine answered this check, `success_score` is null
            and `answered_checks` is 0. `answered_checks` and `failed_checks` count
            over the same engines this score averages.
          title: Success Score
        timestamp:
          description: ISO-8601 UTC timestamp the check was recorded.
          title: Timestamp
          type: string
      required:
      - check_no
      - timestamp
      title: CheckSummary
      type: object
    CompetitionPositionInsight:
      additionalProperties: true
      description: A generated one-line read of the user's position in this ranking.
      properties:
        detail:
          description: One sentence behind the headline.
          title: Detail
          type: string
        label:
          description: Short headline.
          title: Label
          type: string
      required:
      - label
      - detail
      title: CompetitionPositionInsight
      type: object
    CompetitionSummaryBlock:
      additionalProperties: true
      description: 'Where the project stands in this ranking: the numbers the app''s
        summary tile shows.


        Computed by the same code as the app, over the same rows, so every value here

        matches the Competitors page for the same window and filters.'
      properties:
        first_place_name:
          anyOf:
          - type: string
          - type: 'null'
          description: The leader's name, or the runner-up's when you are first. Can
            name a hidden row, as in the app.
          title: First Place Name
        gap_to_first:
          anyOf:
          - type: number
          - type: 'null'
          description: Percentage points (1 decimal place) between your `last_check_share`
            and the leader's, or the runner-up's when you are first. Read from the
            full ranking, so the leader can be a hidden row, as in the app.
          title: Gap To First
        last_check_share:
          anyOf:
          - type: number
          - type: 'null'
          description: '`last_check_share` of that row, a percentage (0-100).'
          title: Last Check Share
        last_check_share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: '`last_check_share_delta` of that row, in percentage points.'
          title: Last Check Share Delta
        position_insight:
          anyOf:
          - $ref: '#/components/schemas/CompetitionPositionInsight'
          - type: 'null'
          description: Generated text describing your position, when one exists for
            this window. Its wording is not a stable contract; do not parse it.
        ranked_entity_count:
          description: 'How many entities the ranking ranked before it was cut to
            the rows returned: the denominator of `user_rank`.'
          title: Ranked Entity Count
          type: integer
        share:
          anyOf:
          - type: number
          - type: 'null'
          description: '`share` of that row, a percentage (0-100, 2 decimal places,
            as the row).'
          title: Share
        share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: '`share_delta` of that row, in percentage points.'
          title: Share Delta
        total_count:
          anyOf:
          - type: integer
          - type: number
          description: 'Summed `count` of every row the ranking returned, hidden ones
            included: a citation, mention or appearance volume, not a row count. With
            `group_by=tag`, the window''s entity-level total, since tag rows overlap.'
          title: Total Count
        user_rank:
          anyOf:
          - type: integer
          - type: 'null'
          description: '`rank` of the project''s best-ranked own row. Null when none
            of your domains or brands is in the ranking.'
          title: User Rank
        user_rank_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: '`rank_delta` of that row; positive means it moved up.'
          title: User Rank Delta
        user_ranks:
          description: Every own row in the ranking, best-ranked first.
          items:
            $ref: '#/components/schemas/CompetitionUserRank'
          title: User Ranks
          type: array
      required:
      - ranked_entity_count
      - total_count
      title: CompetitionSummaryBlock
      type: object
    CompetitionUserRank:
      additionalProperties: true
      description: One of the project's own rows, as the summary reads it.
      properties:
        last_check_share:
          anyOf:
          - type: number
          - type: 'null'
          description: The row's `last_check_share`, a percentage (0-100).
          title: Last Check Share
        last_check_share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: The row's `last_check_share_delta`, in percentage points.
          title: Last Check Share Delta
        name:
          description: The user's domain, brand or URL.
          title: Name
          type: string
        rank:
          description: 'The row''s `rank`: by the most recent check, over every ranked
            entity, hidden ones included.'
          title: Rank
          type: integer
        rank_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: The row's `rank_delta`.
          title: Rank Delta
        share:
          anyOf:
          - type: number
          - type: 'null'
          description: The row's `share`, a percentage (0-100, 2 decimal places).
          title: Share
        share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: The row's `share_delta`, in percentage points.
          title: Share Delta
        total_rank:
          anyOf:
          - type: integer
          - type: 'null'
          description: The row's `total_rank`.
          title: Total Rank
      required:
      - name
      - rank
      title: CompetitionUserRank
      type: object
    CompetitorCitationPromptItem:
      properties:
        citation_count:
          description: Engine answers to this prompt in the window that cite the domain
            or URL, counted as the ranking row they drill from counts them.
          title: Citation Count
          type: integer
        last_cited_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the latest answer citing it in the
            window. Null when unknown.
          title: Last Cited At
        prompt_id:
          description: Identifier of the prompt, as in `GET /v1/projects/{project_id}/prompts`.
          title: Prompt Id
          type: integer
        text:
          description: The prompt's text.
          title: Text
          type: string
      required:
      - prompt_id
      - text
      - citation_count
      - last_cited_at
      title: CompetitorCitationPromptItem
      type: object
    CompetitorCitationPromptListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: Prompts on this page.
          items:
            $ref: '#/components/schemas/CompetitorCitationPromptItem'
          title: Data
          type: array
        domain:
          description: The domain the rows describe, without a leading `www.`, lower-cased.
          title: Domain
          type: string
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - domain
      title: CompetitorCitationPromptListResponse
      type: object
    CompetitorCitationUrlItem:
      properties:
        citation_count:
          description: Engine answers in the window that cite this URL, over the filtered
            prompts.
          title: Citation Count
          type: integer
        last_cited_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the latest answer citing it in the
            window. Null when unknown.
          title: Last Cited At
        url:
          description: The cited page, written exactly as `competitors/sources` names
            it in `name`.
          title: Url
          type: string
      required:
      - url
      - citation_count
      - last_cited_at
      title: CompetitorCitationUrlItem
      type: object
    CompetitorCitationUrlListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: URLs on this page.
          items:
            $ref: '#/components/schemas/CompetitorCitationUrlItem'
          title: Data
          type: array
        domain:
          description: The domain the rows describe, without a leading `www.`, lower-cased.
          title: Domain
          type: string
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - domain
      title: CompetitorCitationUrlListResponse
      type: object
    CompetitorDetailsRespV1:
      description: 'Aggregated facts about one brand.


        ``brand`` echoes the spelling the caller asked for. ``canonical_brand``

        names the entity the facts describe, after the lookup resolved the request

        through the same merge map the mentions ranking uses. ``days`` is the

        window''s length in days after ``date_from``: a window of one day is 0.'
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        brand:
          title: Brand
          type: string
        canonical_brand:
          anyOf:
          - type: string
          - type: 'null'
          title: Canonical Brand
        days:
          title: Days
          type: integer
        facts:
          items:
            $ref: '#/components/schemas/CompetitorFactResp'
          title: Facts
          type: array
        total_distinct_facts:
          title: Total Distinct Facts
          type: integer
      required:
      - applied
      - brand
      - days
      - total_distinct_facts
      - facts
      title: CompetitorDetailsRespV1
      type: object
    CompetitorFactResp:
      properties:
        detail:
          title: Detail
          type: string
        last_seen_check_no:
          anyOf:
          - type: integer
          - type: 'null'
          title: Last Seen Check No
        occurrence_count:
          title: Occurrence Count
          type: integer
        sentiment:
          anyOf:
          - type: string
          - type: 'null'
          title: Sentiment
      required:
      - detail
      - occurrence_count
      title: CompetitorFactResp
      type: object
    CompetitorListItem:
      additionalProperties: true
      properties:
        average_count:
          anyOf:
          - type: number
          - type: 'null'
          description: With group_by=tag, count divided by member_count.
          title: Average Count
        average_last_check_count:
          anyOf:
          - type: number
          - type: 'null'
          description: With group_by=tag, last_check_count divided by member_count.
          title: Average Last Check Count
        average_share:
          anyOf:
          - type: number
          - type: 'null'
          description: With group_by=tag, share divided by member_count, a percentage
            (0-100) like share.
          title: Average Share
        category:
          anyOf:
          - enum:
            - ugc
            - news
            - wiki
            - official
            - social
            - other
            type: string
          - type: 'null'
          description: 'The kind of site the cited domain is: `ugc` (forums, Q&A,
            reviews, blogs and video, where users write the content), `news`, `wiki`,
            `official` (company, brand, product, documentation and government sites),
            `social`, or `other`, which also holds every domain no classification
            covers. Set on `competitors/citations` rows; with `group_by=category`
            each row is a category and this equals `name`. `null` on `competitors/mentions`
            and `competitors/sources`, whose rows are brands and URLs.'
          title: Category
        color:
          anyOf:
          - type: string
          - type: 'null'
          description: With group_by=tag, the tag's hex colour. On other rows, the
            colour the app draws the row in.
          title: Color
        count:
          anyOf:
          - type: integer
          - type: number
          - type: 'null'
          description: Number of citations or mentions in the window. A fraction only
            on a tag row with `aggregate=average`.
          title: Count
        count_delta:
          anyOf:
          - type: integer
          - type: number
          - type: 'null'
          description: '`count` over the second half of the window minus `count` over
            the first half. 0 is a real standstill; null means the first half has
            no data.'
          title: Count Delta
        is_competitor:
          default: false
          description: True when the row carries a competitor mark in the app and
            is not hidden. Always false on tag rows and with `group_by=category`.
          title: Is Competitor
          type: boolean
        is_hidden:
          default: false
          description: True when the row is hidden in the app. Hidden rows appear
            only with `filter=hidden`.
          title: Is Hidden
          type: boolean
        is_user:
          default: false
          description: True on the project's own domains (brands on `competitors/mentions`),
            matched without case or a leading `www.`; on `competitors/sources`, by
            the URL's host. Always false on tag rows and with `group_by=category`.
            Your own rows are kept by every `filter` and by `competitor_tag_ids`.
          title: Is User
          type: boolean
        last_check_count:
          anyOf:
          - type: integer
          - type: number
          - type: 'null'
          description: Citations or mentions in the most recent check. A fraction
            only on a tag row with `aggregate=average`.
          title: Last Check Count
        last_check_share:
          anyOf:
          - type: number
          - type: 'null'
          description: Share (%) of citations or mentions in the most recent check.
          title: Last Check Share
        last_check_share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: Change in `last_check_share` against the previous check, in
            percentage points (2 decimal places). Null when there is no previous check.
          title: Last Check Share Delta
        member_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: With group_by=tag, the tag's members on this tab; the divisor
            of every average_* field.
          title: Member Count
        members:
          anyOf:
          - items:
              $ref: '#/components/schemas/CompetitorTagMemberShare'
            type: array
          - type: 'null'
          description: With group_by=tag, every member with its count and share in
            the window, zero-count members included.
          title: Members
        name:
          description: 'What the row ranks: a cited domain on `competitors/citations`
            (a source category key with `group_by=category`), a canonical brand name
            on `competitors/mentions`, a full URL on `competitors/sources`, and the
            competitor tag name with `group_by=tag`.'
          title: Name
          type: string
        per_engine:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          deprecated: true
          description: Deprecated. Never populated, always null. Narrow with `engines`
            instead.
          title: Per Engine
        prev_total_rank:
          anyOf:
          - type: integer
          - type: 'null'
          description: '`total_rank` over the previous half of the window. Null when
            the row had no count in that half.'
          title: Prev Total Rank
        rank:
          anyOf:
          - type: integer
          - type: 'null'
          description: 'Position by the most recent check''s count over every ranked
            entity, hidden ones included, as the app''s # column shows it. Not re-based
            by `filter`, `competitor_tag_ids` or paging, so the rows of a filtered
            page can skip numbers. Sort by `rank:asc` for the app''s order.'
          title: Rank
        rank_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: Change in `rank` between the two latest checks; positive means
            the row moved up. Null when there is no previous check.
          title: Rank Delta
        rank_delta_period:
          anyOf:
          - type: integer
          - type: 'null'
          description: '`prev_total_rank - total_rank`: positive means the row moved
            up over the window. Null whenever `prev_total_rank` is.'
          title: Rank Delta Period
        share:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of all citations or mentions in the window, as a percentage
            (0-100, 2 decimal places).
          title: Share
        share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: Change in `share` between the two halves of the window, in
            percentage points (2 decimal places). Null when the first half has no
            data.
          title: Share Delta
        shopping_count:
          anyOf:
          - type: integer
          - type: number
          default: 0
          description: 'How many of `count` came from a Google Shopping product panel
            rather than an ordinary cited page. `0 <= shopping_count <= count`, over
            the same window and the same filters as `count`. It is a count rather
            than a per-row kind because the entity decides how far it saturates: on
            `competitors/citations` the row is a domain, which holds both surfaces
            at once - `google.com` carries panels and ordinary results together -
            so this is the split inside the row; on `competitors/sources` the row
            is a URL and every citation of a panel URL is a panel, so the value saturates
            to `count`; on `competitors/mentions` it is always 0, because a brand
            mention in answer text has no citation surface. A panel is not a link
            to a merchant and carries Google''s own host, so a domain row whose `shopping_count`
            is most of its `count` describes product listings rather than a site outranking
            you.'
          title: Shopping Count
        tag_id:
          anyOf:
          - type: integer
          - type: 'null'
          description: With group_by=tag, the competitor tag this row aggregates.
          title: Tag Id
        tags:
          description: Competitor tags the row carries, in name order. On `competitors/sources`,
            the tags of the URL's host. Always empty on hidden rows, tag rows and
            with `group_by=category`.
          items:
            $ref: '#/components/schemas/CompetitorTagRef'
          title: Tags
          type: array
        total_rank:
          anyOf:
          - type: integer
          - type: 'null'
          description: Position by `count` over the whole window rather than by the
            most recent check, over the same population as `rank`.
          title: Total Rank
        trend:
          description: The row's share at each check in the window, a percentage (0-100),
            oldest first.
          items:
            type: number
          title: Trend
          type: array
        trend_counts:
          description: The row's count at each check, index-aligned with `trend` and
            the same length.
          items:
            anyOf:
            - type: integer
            - type: number
          title: Trend Counts
          type: array
      required:
      - name
      title: CompetitorListItem
      type: object
    CompetitorListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: Competitors on this page.
          items:
            $ref: '#/components/schemas/CompetitorListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        summary:
          $ref: '#/components/schemas/CompetitionSummaryBlock'
          description: Where the project stands in this ranking, over every row rather
            than this page.
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - summary
      title: CompetitorListResponse
      type: object
    CompetitorMentionPromptItem:
      properties:
        last_mentioned_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the latest answer mentioning the
            brand in the window. Null when unknown.
          title: Last Mentioned At
        mention_count:
          description: Engine answers to this prompt in the window that mention the
            brand, counted as the `competitors/mentions` row counts them.
          title: Mention Count
          type: integer
        prompt_id:
          description: Identifier of the prompt, as in `GET /v1/projects/{project_id}/prompts`.
          title: Prompt Id
          type: integer
        text:
          description: The prompt's text.
          title: Text
          type: string
      required:
      - prompt_id
      - text
      - mention_count
      - last_mentioned_at
      title: CompetitorMentionPromptItem
      type: object
    CompetitorMentionPromptListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        brand:
          description: The brand as you sent it.
          title: Brand
          type: string
        canonical_brand:
          anyOf:
          - type: string
          - type: 'null'
          description: The merged brand the counts describe, after the same alias
            map `competitors/mentions` uses resolved `brand`.
          title: Canonical Brand
        data:
          description: Prompts on this page.
          items:
            $ref: '#/components/schemas/CompetitorMentionPromptItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - brand
      title: CompetitorMentionPromptListResponse
      type: object
    CompetitorSerpPromptItem:
      properties:
        best_position:
          description: The domain's best organic position for this prompt in the window,
            1-10.
          title: Best Position
          type: integer
        last_ranked_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the latest top-10 appearance in the
            window. Null when unknown.
          title: Last Ranked At
        prompt_id:
          description: Identifier of the prompt, as in `GET /v1/projects/{project_id}/prompts`.
          title: Prompt Id
          type: integer
        rank_count:
          description: Top-10 organic results of this prompt's checks in the window
            held by the domain, counted as the organic ranking row counts them.
          title: Rank Count
          type: integer
        text:
          description: The prompt's text.
          title: Text
          type: string
      required:
      - prompt_id
      - text
      - rank_count
      - best_position
      - last_ranked_at
      title: CompetitorSerpPromptItem
      type: object
    CompetitorSerpPromptListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: Prompts on this page.
          items:
            $ref: '#/components/schemas/CompetitorSerpPromptItem'
          title: Data
          type: array
        domain:
          description: The domain the rows describe, without a leading `www.`, lower-cased.
          title: Domain
          type: string
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - domain
      title: CompetitorSerpPromptListResponse
      type: object
    CompetitorSerpUrlItem:
      properties:
        avg_position:
          description: The URL's mean organic position over those appearances, 1-10,
            at one decimal.
          title: Avg Position
          type: number
        last_ranked_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the latest top-10 appearance in the
            window. Null when unknown.
          title: Last Ranked At
        rank_count:
          description: Top-10 organic appearances of this URL in the window.
          title: Rank Count
          type: integer
        url:
          description: The ranked page's URL.
          title: Url
          type: string
      required:
      - url
      - rank_count
      - avg_position
      - last_ranked_at
      title: CompetitorSerpUrlItem
      type: object
    CompetitorSerpUrlListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: URLs on this page.
          items:
            $ref: '#/components/schemas/CompetitorSerpUrlItem'
          title: Data
          type: array
        domain:
          description: The domain the rows describe, without a leading `www.`, lower-cased.
          title: Domain
          type: string
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - domain
      title: CompetitorSerpUrlListResponse
      type: object
    CompetitorSourcePromptListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: Prompts on this page.
          items:
            $ref: '#/components/schemas/CompetitorCitationPromptItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
        url:
          description: The source URL the rows describe, as you sent it.
          title: Url
          type: string
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - url
      title: CompetitorSourcePromptListResponse
      type: object
    CompetitorTagCreateResponse:
      additionalProperties: true
      description: The created competitor tag. Both counts are always 0 on create.
      properties:
        brand_count:
          default: 0
          description: Brands carrying this tag.
          title: Brand Count
          type: integer
        color:
          description: Hex colour of the competitor tag (#RRGGBB).
          title: Color
          type: string
        domain_count:
          default: 0
          description: Domains carrying this tag.
          title: Domain Count
          type: integer
        id:
          description: Competitor tag identifier.
          title: Id
          type: integer
        name:
          description: Display name of the competitor tag.
          title: Name
          type: string
      required:
      - id
      - name
      - color
      title: CompetitorTagCreateResponse
      type: object
    CompetitorTagListItem:
      additionalProperties: true
      properties:
        brand_count:
          default: 0
          description: Brands carrying this tag.
          title: Brand Count
          type: integer
        color:
          description: Hex colour of the competitor tag (#RRGGBB).
          title: Color
          type: string
        domain_count:
          default: 0
          description: Domains carrying this tag.
          title: Domain Count
          type: integer
        id:
          description: Competitor tag identifier.
          title: Id
          type: integer
        name:
          description: Display name of the competitor tag.
          title: Name
          type: string
      required:
      - id
      - name
      - color
      title: CompetitorTagListItem
      type: object
    CompetitorTagListResponse:
      additionalProperties: true
      properties:
        data:
          description: Competitor tags on this page.
          items:
            $ref: '#/components/schemas/CompetitorTagListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: CompetitorTagListResponse
      type: object
    CompetitorTagMemberInput:
      additionalProperties: false
      properties:
        name:
          description: The domain (a host name such as 'acme.com', not a URL) or the
            brand name.
          maxLength: 255
          minLength: 1
          title: Name
          type: string
        type:
          description: '''domain'' for a website host name, ''brand'' for a brand
            name.'
          enum:
          - domain
          - brand
          title: Type
          type: string
      required:
      - type
      - name
      title: CompetitorTagMemberInput
      type: object
    CompetitorTagMemberItem:
      additionalProperties: true
      properties:
        domain:
          anyOf:
          - type: string
          - type: 'null'
          description: The domain for a 'domain' member; null for a brand.
          title: Domain
        id:
          description: Identifier of the tracked competitor, the same id the tracked-competitors
            roster returns.
          title: Id
          type: string
        name:
          description: The domain or the brand name.
          title: Name
          type: string
        type:
          description: '''domain'' or ''brand''.'
          title: Type
          type: string
      required:
      - id
      - type
      - name
      title: CompetitorTagMemberItem
      type: object
    CompetitorTagMemberListResponse:
      additionalProperties: true
      properties:
        data:
          description: Members on this page.
          items:
            $ref: '#/components/schemas/CompetitorTagMemberItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: CompetitorTagMemberListResponse
      type: object
    CompetitorTagMemberShare:
      additionalProperties: true
      properties:
        count:
          description: The member's citations or mentions in the window.
          title: Count
          type: integer
        name:
          description: The member domain or canonical brand name.
          title: Name
          type: string
        share:
          description: The member's share, in the same unit as the row's share.
          title: Share
          type: number
      required:
      - name
      - count
      - share
      title: CompetitorTagMemberShare
      type: object
    CompetitorTagMembersAddResponse:
      additionalProperties: true
      properties:
        data:
          description: The members written, one per request item, in request order.
          items:
            $ref: '#/components/schemas/CompetitorTagMemberItem'
          title: Data
          type: array
      required:
      - data
      title: CompetitorTagMembersAddResponse
      type: object
    CompetitorTagRef:
      additionalProperties: true
      properties:
        color:
          description: Hex colour of the competitor tag (#RRGGBB).
          title: Color
          type: string
        id:
          description: Competitor tag identifier.
          title: Id
          type: integer
        name:
          description: Display name of the competitor tag.
          title: Name
          type: string
      required:
      - id
      - name
      - color
      title: CompetitorTagRef
      type: object
    CompetitorTagUpdateResponse:
      additionalProperties: true
      description: The competitor tag's state after the update.
      properties:
        color:
          description: Hex colour of the competitor tag (#RRGGBB).
          title: Color
          type: string
        id:
          description: Competitor tag identifier.
          title: Id
          type: integer
        name:
          description: Display name of the competitor tag.
          title: Name
          type: string
      required:
      - id
      - name
      - color
      title: CompetitorTagUpdateResponse
      type: object
    CompetitorTrendSeries:
      additionalProperties: true
      description: 'A per-competitor share series across the window.


        Each entry''s ``data`` array is aligned to the date axis emitted at the

        top level of the response (look for a ``dates`` sibling, or rely on

        one value per day within the requested window). _AllowExtra permits

        any additional fields the upstream attaches per series.'
      properties:
        average_data:
          anyOf:
          - items:
              anyOf:
              - type: number
              - type: 'null'
            type: array
          - type: 'null'
          description: With group_by=tag, each data value divided by member_count,
            rounded to 2 decimals, nulls kept. Computed from the totals whatever `aggregate`
            is.
          title: Average Data
        color:
          anyOf:
          - type: string
          - type: 'null'
          description: With group_by=tag, the tag's hex colour.
          title: Color
        counts:
          anyOf:
          - items:
              anyOf:
              - type: integer
              - type: number
              - type: 'null'
            type: array
          - type: 'null'
          description: Per-date counts behind `data`, aligned to the same axis. Fractions
            only on a tag series with `aggregate=average`.
          title: Counts
        data:
          anyOf:
          - items:
              anyOf:
              - type: number
              - type: 'null'
            type: array
          - type: 'null'
          description: Per-date share values aligned to the response's date axis.
            Entries may be null for dates with no data.
          title: Data
        is_user:
          anyOf:
          - type: boolean
          - type: 'null'
          description: True when this series belongs to one of the project's own brands/domains.
          title: Is User
        member_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: With group_by=tag, the tag's members of the requested type;
            the divisor of average_data.
          title: Member Count
        name:
          description: Competitor brand or domain name.
          title: Name
          type: string
        tag_id:
          anyOf:
          - type: integer
          - type: 'null'
          description: With group_by=tag, the competitor tag this series aggregates.
          title: Tag Id
      required:
      - name
      title: CompetitorTrendSeries
      type: object
    CompetitorTrendsResponse:
      additionalProperties: true
      description: 'Competitor trend time series.


        Returned as one series per competitor with a shared date axis at the

        top level rather than the historical per-date bucket form. _AllowExtra

        on this and the inner series lets upstream evolve sibling fields

        (e.g. ``dates``, hero metrics) without breaking the public contract.'
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        series:
          description: One time-series per competitor. Values align to the response's
            date axis.
          items:
            $ref: '#/components/schemas/CompetitorTrendSeries'
          title: Series
          type: array
      required:
      - applied
      - series
      title: CompetitorTrendsResponse
      type: object
    CreateBrandFactBody:
      additionalProperties: false
      properties:
        category:
          description: 'What the fact is about: `price`, `plan`, `feature`, `location`
            or `other`.'
          enum:
          - price
          - plan
          - feature
          - location
          - other
          title: Category
          type: string
        statement:
          description: The fact, one claim about your own business, 1 to 200 characters
            after trimming. Do not enter personal data.
          maxLength: 200
          minLength: 1
          title: Statement
          type: string
      required:
      - statement
      - category
      title: CreateBrandFactBody
      type: object
    CreateCompetitorTagBody:
      additionalProperties: false
      properties:
        color:
          anyOf:
          - maxLength: 7
            minLength: 7
            type: string
          - type: 'null'
          description: Hex colour '#RRGGBB'. Omit it to take the next colour of the
            competitor tag palette.
          title: Color
        name:
          description: Display name, 1 to 120 characters after trimming. Unique in
            the project, ignoring case.
          maxLength: 120
          minLength: 1
          title: Name
          type: string
      required:
      - name
      title: CreateCompetitorTagBody
      type: object
    CreatePromptBody:
      additionalProperties: false
      properties:
        monitoring_status:
          default: true
          description: Whether to monitor this prompt on the project schedule.
          title: Monitoring Status
          type: boolean
        tag_ids:
          description: IDs of existing tags to attach at creation. At most 100, each
            1-2147483647.
          items:
            maximum: 2147483647
            minimum: 1
            type: integer
          maxItems: 100
          title: Tag Ids
          type: array
        text:
          description: The prompt text to track. 1-500 characters.
          maxLength: 500
          minLength: 1
          title: Text
          type: string
        url_ids:
          description: IDs of existing URLs to associate at creation, each 1-2147483647.
          items:
            maximum: 2147483647
            minimum: 1
            type: integer
          title: Url Ids
          type: array
      required:
      - text
      title: CreatePromptBody
      type: object
    CreateTagBody:
      additionalProperties: false
      properties:
        color:
          description: Hex color in '#RRGGBB' form.
          maxLength: 7
          minLength: 7
          title: Color
          type: string
        name:
          description: Display name. 1-50 characters.
          maxLength: 50
          minLength: 1
          title: Name
          type: string
      required:
      - name
      - color
      title: CreateTagBody
      type: object
    CreateUrlBody:
      additionalProperties: false
      properties:
        url:
          description: Absolute URL to associate with the project. Up to 1024 characters.
          maxLength: 1024
          minLength: 1
          title: Url
          type: string
      required:
      - url
      title: CreateUrlBody
      type: object
    DashboardLlmTrendsResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        checks:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: The date axis, one ISO-8601 date per bucket. Every array under
            `engines` is index-aligned with this, so read a value's date from here
            rather than expecting one on the value.
          title: Checks
        engine_summary:
          additionalProperties:
            $ref: '#/components/schemas/OverviewEngineSummary'
          description: 'Each engine''s latest score in the window and its move, keyed
            by public engine name: the app''s best and worst engine strip. Equal to
            the overview''s `engine_summary` for the same window, `engines` and `tag_ids`
            when the overview is read without `url_ids`; this endpoint takes no `url_ids`.
            An engine with neither an answered nor a failed check in the window has
            no key.'
          title: Engine Summary
          type: object
        engines:
          additionalProperties:
            $ref: '#/components/schemas/EngineTrendSeries'
          description: Per-engine series, keyed by public engine name. Only engines
            with a bucket in the window appear.
          title: Engines
          type: object
      required:
      - applied
      - engines
      title: DashboardLlmTrendsResponse
      type: object
    DashboardOverviewResponse:
      additionalProperties: true
      description: 'Everything the project overview reports for one window.


        Four of these blocks - `trends`, `top_citations`, `top_brands` and

        `tag_success_score` - are keyed by public engine name plus `combined`.

        They are null rather than empty when the window holds no checks at all, so

        a null block and an all-empty block mean different things: nothing was

        measured, versus nothing was found.'
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        engine_summary:
          additionalProperties:
            $ref: '#/components/schemas/OverviewEngineSummary'
          description: 'Each engine''s latest score in the window and its move, keyed
            by public engine name: the app''s engine strip. An engine with neither
            an answered nor a failed check in the window has no key.'
          title: Engine Summary
          type: object
        insight:
          anyOf:
          - $ref: '#/components/schemas/OverviewInsightBlock'
          - type: 'null'
          description: Generated written summary of the latest check batch. Null until
            the out-of-band pipeline has produced one.
        kpis:
          anyOf:
          - $ref: '#/components/schemas/OverviewKpis'
          - type: 'null'
          description: The headline numbers, as the app's KPI cards show them for
            this window. Null when `trends` is.
        per_engine:
          anyOf:
          - additionalProperties:
              $ref: '#/components/schemas/OverviewEngineScore'
            type: object
          - type: 'null'
          deprecated: true
          description: 'Deprecated: use `engine_summary`. Score per engine from the
            most recent check batch in which that engine answered, keyed by public
            engine name. It reports a batch rather than the window, so it does not
            move with `date_from`/`date_to`, and it ignores `engines`, `tag_ids` and
            `url_ids`.'
          title: Per Engine
        tag_success_score:
          anyOf:
          - $ref: '#/components/schemas/TagSuccessScoreBlock'
          - type: 'null'
          description: Success score broken down by project tag, per engine.
        thresholds:
          anyOf:
          - $ref: '#/components/schemas/ProjectThresholdsBlock'
          - type: 'null'
          description: The project's good/fair display bands for the headline metrics.
            These band the numbers on this response for presentation; they are not
            inputs to any of them, so the bands do not change `success_score`. The
            score is measured on the project's own prompts, engines, country and cadence,
            so compare it with the project's own past, never with another project's.
        top_brands:
          anyOf:
          - $ref: '#/components/schemas/TopBrandsBlock'
          - type: 'null'
          deprecated: true
          description: 'Deprecated: use `top_competitors`. Most-mentioned brands per
            engine, straight from the upstream ranking: rows you hid in the app are
            included and no row says which brand is yours.'
        top_citations:
          anyOf:
          - $ref: '#/components/schemas/TopCitationsBlock'
          - type: 'null'
          deprecated: true
          description: 'Deprecated: use `top_competitors`. Most-cited domains per
            engine, straight from the upstream ranking: rows you hid in the app are
            included and no row says which domain is yours.'
        top_competitors:
          anyOf:
          - $ref: '#/components/schemas/OverviewTopCompetitors'
          - type: 'null'
          description: The most-cited domains and most-mentioned brands, as the app's
            Competitive Landscape shows them. Null when `trends` is.
        total_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: 'Without `tag_ids` or `url_ids`: prompts with at least one
            check that did not fail in the window. With either filter: the prompts
            the filter matches. This counts prompts, not checks.'
          title: Total Count
        trends:
          anyOf:
          - $ref: '#/components/schemas/OverviewTrendsBlock'
          - type: 'null'
          description: Daily success-score trend lines, one series per engine.
        uncertainty:
          anyOf:
          - $ref: '#/components/schemas/OverviewUncertainty'
          - type: 'null'
          description: Likely ranges and change verdicts for the headline KPIs, for
            the same two days as `kpis`. Indicative outputs of the method, not a validated
            test. Null when nothing was measured, when the block is not available,
            or when it describes different days than `kpis`.
      required:
      - applied
      title: DashboardOverviewResponse
      type: object
    DashboardUgcImpactResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: UGC platforms ranked by citation count.
          items:
            $ref: '#/components/schemas/UgcImpactRow'
          title: Data
          type: array
        total:
          description: Total UGC citations across all platforms in the window.
          title: Total
          type: integer
      required:
      - applied
      - data
      - total
      title: DashboardUgcImpactResponse
      type: object
    DismissProblemBody:
      additionalProperties: false
      properties:
        reason:
          anyOf:
          - enum:
            - not_wrong
            - harmless
            - other
            type: string
          - type: 'null'
          description: 'Why the problem is dismissed: `not_wrong` (the answers are
            right and the fact is out of date, or the judgement was wrong), `harmless`
            or `other`. Optional.'
          title: Reason
      title: DismissProblemBody
      type: object
    EngineResultResp:
      additionalProperties: true
      properties:
        brand_mention:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Brand Mention
        brands:
          default: []
          items:
            $ref: '#/components/schemas/BrandResp'
          title: Brands
          type: array
        citations:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Citations
        error_message:
          anyOf:
          - type: string
          - type: 'null'
          title: Error Message
        failed:
          anyOf:
          - type: boolean
          - type: 'null'
          description: 'The engine''s run of this check failed and returned no answer:
            true exactly when `error_message` is set. `brand_mention`, `citations`
            and `sentiment` are then null, and the check is left out of every rate.'
          title: Failed
        favored_brand:
          anyOf:
          - type: string
          - type: 'null'
          title: Favored Brand
        response_length:
          anyOf:
          - type: integer
          - type: 'null'
          title: Response Length
        response_text:
          anyOf:
          - type: string
          - type: 'null'
          title: Response Text
        sentiment:
          anyOf:
          - type: string
          - type: 'null'
          title: Sentiment
        sources:
          default: []
          items:
            $ref: '#/components/schemas/CheckSourceResp'
          title: Sources
          type: array
      title: EngineResultResp
      type: object
    EngineTrendSeries:
      additionalProperties: true
      description: 'One engine''s metric series, as parallel arrays.


        Every array here is index-aligned with the response''s `checks` array, which

        is the date axis: `success_score[2]` is the score on `checks[2]`. A `null`

        rate is a bucket with no answered check for that engine: either no check

        ran, or every check failed, in which case `failed_count[i]` says how many.

        That is not the same as a zero. Failed checks are excluded from every rate

        and every `*_denominator`. The rate fields are percentages on a 0-100

        scale; the `*_count` and `*_denominator` fields are the integers they were

        computed from, so a consumer can re-aggregate over a longer period rather

        than averaging averages.'
      properties:
        citations_count:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answers that cited a tracked URL, per bucket.
          title: Citations Count
        citations_denominator:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answered checks analysed for citations, per bucket. Failed
            checks are excluded.
          title: Citations Denominator
        citations_rate:
          anyOf:
          - items:
              anyOf:
              - type: number
              - type: 'null'
            type: array
          - type: 'null'
          description: Share of answered checks citing a tracked URL, per bucket.
          title: Citations Rate
        failed_count:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Failed checks per bucket, left out of every rate and denominator
            above. A bucket whose rates are null and whose `failed_count` is above
            0 is one in which every check on this engine failed.
          title: Failed Count
        mentions_count:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answers that named a tracked brand, per bucket.
          title: Mentions Count
        mentions_denominator:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answered checks analysed for mentions, per bucket. Failed checks
            are excluded.
          title: Mentions Denominator
        mentions_rate:
          anyOf:
          - items:
              anyOf:
              - type: number
              - type: 'null'
            type: array
          - type: 'null'
          description: Share of answered checks naming a tracked brand, per bucket.
          title: Mentions Rate
        sentiment_denominator:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answers carrying any sentiment classification, per bucket.
          title: Sentiment Denominator
        sentiment_mixed_count:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answers classified mixed, per bucket.
          title: Sentiment Mixed Count
        sentiment_negative_count:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answers classified negative, per bucket.
          title: Sentiment Negative Count
        sentiment_positive_count:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answers classified positive, per bucket.
          title: Sentiment Positive Count
        sentiment_score:
          anyOf:
          - items:
              anyOf:
              - type: number
              - type: 'null'
            type: array
          - type: 'null'
          description: Sentiment score across classified answers, per bucket.
          title: Sentiment Score
        success_count:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answers counted as a success, per bucket.
          title: Success Count
        success_denominator:
          anyOf:
          - items:
              type: integer
            type: array
          - type: 'null'
          description: Answered checks the success rate was taken over, per bucket.
            Failed checks are excluded.
          title: Success Denominator
        success_score:
          anyOf:
          - items:
              anyOf:
              - type: number
              - type: 'null'
            type: array
          - type: 'null'
          description: Success score per bucket.
          title: Success Score
      title: EngineTrendSeries
      type: object
    EngineUncertainty:
      description: One engine's likely ranges and verdicts over the block's two days.
      properties:
        changes:
          $ref: '#/components/schemas/UncertaintyKpiChanges'
          description: Verdicts on this engine's changes, `latest_date` against `first_date`.
        first_date:
          description: ISO-8601 date (`YYYY-MM-DD`) of the first day this entry judged.
          title: First Date
          type: string
        kpis:
          $ref: '#/components/schemas/UncertaintyKpiIntervals'
          description: This engine's likely ranges on `latest_date`.
        latest_date:
          description: ISO-8601 date (`YYYY-MM-DD`) of the latest day this entry judged.
            It is the block's `latest_date`, equal to the engine's own `engine_summary.last_check`.
          title: Latest Date
          type: string
      required:
      - latest_date
      - first_date
      - kpis
      - changes
      title: EngineUncertainty
      type: object
    GscAppliedFilters:
      additionalProperties: false
      description: 'The `applied` echo of a Search Console read: the window plus the
        property read.'
      properties:
        date_from:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: First day read, `YYYY-MM-DD` (UTC). Null only on `checks/answers`
            in `latest` or `check_no` mode, which read no window.
          title: Date From
        date_to:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: Last day read, `YYYY-MM-DD` (UTC), after a `date_to` later
            than today is clamped to today. Null exactly when `date_from` is.
          title: Date To
        engines:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: 'Public engine names the numbers cover: the requested `engines`
            the workspace''s plan includes, or the plan''s engines when `engines`
            is omitted. An engine you asked for that the plan lacks is not here, which
            is how a silently narrowed filter shows. Null where the read has no per-engine
            data (Search Console, the organic SERP rankings).'
          title: Engines
        history_from:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: The oldest day of Search Console history held for this property,
            `YYYY-MM-DD`. Days before it were never loaded, so a window starting earlier
            is partly empty by absence, not by zero traffic. Null when nothing is
            held yet or no property is connected.
          title: History From
        property:
          anyOf:
          - type: string
          - type: 'null'
          description: The Search Console property the figures come from, exactly
            as Google names it (`sc-domain:example.com` or a URL prefix). It is the
            property you asked for with `property`, or the project's primary property
            (the earliest connected) when you sent none. Null when the project has
            no connected property.
          title: Property
      required:
      - date_from
      - date_to
      - engines
      - property
      - history_from
      title: GscAppliedFilters
      type: object
    GscPageRow:
      additionalProperties: true
      description: 'One indexed page''s Search Console totals over the window.


        Page-level data includes anonymized-query traffic, so a page''s clicks and

        impressions are at least as large as the query-level totals.'
      properties:
        clicks:
          description: Clicks summed over the window. Page-level Search Console data,
            which includes the traffic from anonymized queries, so it is at least
            as large as the query-level totals from `/gsc/queries` and `/gsc/trends`.
          title: Clicks
          type: integer
        clicks_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: Change in `clicks`. This window minus the equal-length window
            just before `date_from`. Null when there is no stored data for this row
            in that earlier window. A count, not a percentage.
          title: Clicks Delta
        ctr:
          description: 'Click-through rate as a 0-1 fraction, 4 decimal places: clicks
            divided by impressions over the window, 0.0 when impressions are 0.'
          title: Ctr
          type: number
        impressions:
          description: Impressions summed over the window. Page-level Search Console
            data, which includes the traffic from anonymized queries, so it is at
            least as large as the query-level totals from `/gsc/queries` and `/gsc/trends`.
          title: Impressions
          type: integer
        impressions_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: Change in `impressions`. This window minus the equal-length
            window just before `date_from`. Null when there is no stored data for
            this row in that earlier window. A count, not a percentage.
          title: Impressions Delta
        page:
          description: The indexed page URL.
          title: Page
          type: string
        position:
          description: 'Average position in Google''s results, 1 decimal place: the
            impressions-weighted mean of Google''s daily average position over the
            window. Server-computed, so recomputing it from rounded daily values gives
            slightly different figures.'
          title: Position
          type: number
      required:
      - page
      - impressions
      - clicks
      - ctr
      - position
      title: GscPageRow
      type: object
    GscPagesResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/GscAppliedFilters'
          description: The window and property this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: GSC page metrics on this page.
          items:
            $ref: '#/components/schemas/GscPageRow'
          title: Data
          type: array
        gsc_connected:
          description: Whether the project has a connected Search Console property.
            False means the data is empty, including when the property was disconnected
            while the request was served.
          title: Gsc Connected
          type: boolean
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - gsc_connected
      title: GscPagesResponse
      type: object
    GscQueriesResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/GscAppliedFilters'
          description: The window and property this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: GSC query metrics on this page.
          items:
            $ref: '#/components/schemas/GscQueryRow'
          title: Data
          type: array
        gsc_connected:
          description: Whether the project has a connected Search Console property.
            False means the data is empty, including when the property was disconnected
            while the request was served.
          title: Gsc Connected
          type: boolean
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - gsc_connected
      title: GscQueriesResponse
      type: object
    GscQueryRow:
      additionalProperties: true
      description: 'One search query''s Search Console totals over the window.


        Query-level data: Google withholds anonymized queries, so these totals are

        lower than the property''s site-wide figures and than the page-level rows.'
      properties:
        clicks:
          description: Clicks summed over the window. Query-level Search Console data.
            Google withholds anonymized queries from query-level data, so these totals
            are lower than the property's site-wide clicks and impressions and lower
            than the sum of `/gsc/pages` rows.
          title: Clicks
          type: integer
        clicks_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: Change in `clicks`. This window minus the equal-length window
            just before `date_from`. Null when there is no stored data for this row
            in that earlier window. A count, not a percentage.
          title: Clicks Delta
        ctr:
          description: 'Click-through rate as a 0-1 fraction, 4 decimal places: clicks
            divided by impressions over the window, 0.0 when impressions are 0.'
          title: Ctr
          type: number
        impressions:
          description: Impressions summed over the window. Query-level Search Console
            data. Google withholds anonymized queries from query-level data, so these
            totals are lower than the property's site-wide clicks and impressions
            and lower than the sum of `/gsc/pages` rows.
          title: Impressions
          type: integer
        impressions_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: Change in `impressions`. This window minus the equal-length
            window just before `date_from`. Null when there is no stored data for
            this row in that earlier window. A count, not a percentage.
          title: Impressions Delta
        position:
          description: 'Average position in Google''s results, 1 decimal place: the
            impressions-weighted mean of Google''s daily average position over the
            window. Server-computed, so recomputing it from rounded daily values gives
            slightly different figures.'
          title: Position
          type: number
        query:
          description: The search query.
          title: Query
          type: string
      required:
      - query
      - impressions
      - clicks
      - ctr
      - position
      title: GscQueryRow
      type: object
    GscTrendPoint:
      additionalProperties: true
      description: One day's Search Console totals. Query-level data, like `/gsc/queries`.
      properties:
        clicks:
          description: Clicks on this day. Query-level Search Console data. Google
            withholds anonymized queries from query-level data, so these totals are
            lower than the property's site-wide clicks and impressions and lower than
            the sum of `/gsc/pages` rows.
          title: Clicks
          type: integer
        date:
          description: The day, `YYYY-MM-DD`.
          title: Date
          type: string
        impressions:
          description: Impressions on this day. Query-level Search Console data. Google
            withholds anonymized queries from query-level data, so these totals are
            lower than the property's site-wide clicks and impressions and lower than
            the sum of `/gsc/pages` rows.
          title: Impressions
          type: integer
      required:
      - date
      - impressions
      - clicks
      title: GscTrendPoint
      type: object
    GscTrendsResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/GscAppliedFilters'
          description: The window and property this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: One point per day that has stored data, oldest first. Days
            without data are left out rather than zero-filled, and the last 2-3 days
            are usually missing because Google reports Search Console data with a
            lag.
          items:
            $ref: '#/components/schemas/GscTrendPoint'
          title: Data
          type: array
        gsc_connected:
          description: Whether the project has a connected Search Console property.
            False means the data is empty, including when the property was disconnected
            while the request was served.
          title: Gsc Connected
          type: boolean
        total:
          anyOf:
          - type: integer
          - type: 'null'
          description: The number of points in `data`. Omitted when `gsc_connected`
            is false.
          title: Total
      required:
      - applied
      - data
      - gsc_connected
      title: GscTrendsResponse
      type: object
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          title: Detail
          type: array
      title: HTTPValidationError
      type: object
    LatestCheckBingAiOverviewBlock:
      additionalProperties: true
      description: 'The Bing AI Overview answer for the check, in its older standalone
        shape.


        Deprecated: duplicates `engines.bing_ai_overview`.'
      properties:
        bao_response_status:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Bing returned an AI Overview for the prompt.
          title: Bao Response Status
        bao_response_text:
          anyOf:
          - type: string
          - type: 'null'
          description: The AI Overview text.
          title: Bao Response Text
        brand_mention:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The overview named the project's brand.
          title: Brand Mention
        citations:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The overview cited one of the project's tracked URLs.
          title: Citations
        error_message:
          anyOf:
          - type: string
          - type: 'null'
          description: Why the overview could not be read, when it failed.
          title: Error Message
        followup_queries:
          anyOf:
          - items: {}
            type: array
          - type: 'null'
          description: Follow-up queries Bing suggested.
          title: Followup Queries
        leading_brand:
          anyOf:
          - type: string
          - type: 'null'
          description: The brand the overview favoured, if any.
          title: Leading Brand
        provided_links:
          anyOf:
          - type: boolean
          - type: 'null'
          description: The overview carried links to sources.
          title: Provided Links
        sentiment:
          anyOf:
          - type: string
          - type: 'null'
          description: Sentiment of the overview towards the project's brand.
          title: Sentiment
        subtopics:
          anyOf:
          - items: {}
            type: array
          - type: 'null'
          description: Subtopics the overview covered.
          title: Subtopics
      title: LatestCheckBingAiOverviewBlock
      type: object
    LatestCheckEngineResultResp:
      additionalProperties: true
      description: 'One engine''s result on the latest check, in the check-detail
        shape.


        Also carries the deprecated `cited` / `mentioned` flags, always null.'
      properties:
        brand_mention:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Brand Mention
        brands:
          default: []
          items:
            $ref: '#/components/schemas/BrandResp'
          title: Brands
          type: array
        citations:
          anyOf:
          - type: boolean
          - type: 'null'
          title: Citations
        cited:
          anyOf:
          - type: boolean
          - type: 'null'
          deprecated: true
          description: 'Deprecated. Always null on this endpoint; `citations` carries
            the same answer for this engine. The listing''s per-engine `cited` flag:
            Read it from the first row of `GET /v1/projects/{project_id}/prompts/{prompt_id}/checks`
            (`limit=1`), which is the latest check.'
          title: Cited
        error_message:
          anyOf:
          - type: string
          - type: 'null'
          title: Error Message
        failed:
          anyOf:
          - type: boolean
          - type: 'null'
          description: 'The engine''s run of this check failed and returned no answer:
            true exactly when `error_message` is set. `brand_mention`, `citations`
            and `sentiment` are then null, and the check is left out of every rate.'
          title: Failed
        favored_brand:
          anyOf:
          - type: string
          - type: 'null'
          title: Favored Brand
        mentioned:
          anyOf:
          - type: boolean
          - type: 'null'
          deprecated: true
          description: 'Deprecated. Always null on this endpoint; `brand_mention`
            carries the same answer for this engine. The listing''s per-engine `mentioned`
            flag: Read it from the first row of `GET /v1/projects/{project_id}/prompts/{prompt_id}/checks`
            (`limit=1`), which is the latest check.'
          title: Mentioned
        response_length:
          anyOf:
          - type: integer
          - type: 'null'
          title: Response Length
        response_text:
          anyOf:
          - type: string
          - type: 'null'
          title: Response Text
        sentiment:
          anyOf:
          - type: string
          - type: 'null'
          title: Sentiment
        sources:
          default: []
          items:
            $ref: '#/components/schemas/CheckSourceResp'
          title: Sources
          type: array
      title: LatestCheckEngineResultResp
      type: object
    LimitsResponse:
      additionalProperties: true
      properties:
        addon_active:
          description: 'Always `true` here, and not an entitlement check. A workspace
            whose API Access add-on is off never reaches this response body: the request
            is refused upstream with 403 `API_ADDON_INACTIVE`.'
          title: Addon Active
          type: boolean
        day_remaining:
          description: Requests still available before the daily cap is hit.
          title: Day Remaining
          type: integer
        day_used:
          description: Requests counted against today's daily quota so far.
          title: Day Used
          type: integer
        rate_limits:
          $ref: '#/components/schemas/RateLimitsBlock'
          description: Per-token and workspace rate-limit configuration.
        reset_at:
          anyOf:
          - type: string
          - type: 'null'
          description: Reserved for a future absolute reset timestamp; always `null`.
            The daily counter resets at 00:00 UTC, so compute the next reset client-side
            rather than reading it from this field.
          title: Reset At
      required:
      - addon_active
      - rate_limits
      - day_used
      - day_remaining
      title: LimitsResponse
      type: object
    OverviewEngineScore:
      additionalProperties: true
      description: One engine's score from the latest check batch in which it answered,
        and its move.
      properties:
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on this engine in the scored batch, left out
            of `success_score`. When `success_score` is null (no batch in which this
            engine answered), the failed checks on this engine in the newest check
            batch. Null when the batch predates failed-check counts.
          title: Failed Checks
        newer_failed_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 date (`YYYY-MM-DD`) of the newest check batch in which
            this engine's checks all failed, newer than the scored batch. Null when
            there is none.
          title: Newer Failed At
        newer_failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on this engine in check batches newer than the
            scored batch, in each of which every check on this engine failed. 0 when
            no newer batch has a failed check on this engine. Null when the scored
            batch and every newer one predate failed-check counts, and for an engine
            with no scored batch.
          title: Newer Failed Checks
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Success score for the most recent check batch in which this
            engine answered (0-100). Failed checks are excluded. A newer batch in
            which every check on this engine failed does not replace it; `newer_failed_checks`
            reports it. Null when the engine has answered in no check batch; `failed_checks`
            then counts its failed checks in the newest check batch.
          title: Success Score
        success_score_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: Change against the previous batch, in points. Null with no
            previous batch.
          title: Success Score Delta
      title: OverviewEngineScore
      type: object
    OverviewEngineSummary:
      additionalProperties: true
      description: 'One engine''s latest score in the window and its move: the app''s
        engine strip.


        Read from that engine''s `trends` series by the same shared code the app''s

        Overview and LLM Trends pages use, so it equals the strip for the same

        window and filters. The overview''s block also equals

        `GET .../dashboard/llm-trends` `engine_summary` for the same window,

        `engines` and `tag_ids` when `url_ids` is not sent: the overview narrows

        by `url_ids` and llm-trends takes no `url_ids`.'
      properties:
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on this engine on `last_check`. When `success_score`
            is null, every failed check on it in the window. Null when the data predates
            failed-check counts.
          title: Failed Checks
        first_check:
          anyOf:
          - type: string
          - type: 'null'
          description: '`YYYY-MM-DD` of the first day this engine answered in the
            window: the delta''s basis. Equal to `last_check` when it answered on
            one day only.'
          title: First Check
        last_check:
          anyOf:
          - type: string
          - type: 'null'
          description: '`YYYY-MM-DD` of the day `success_score` is read from.'
          title: Last Check
        newer_failed_at:
          anyOf:
          - type: string
          - type: 'null'
          description: '`YYYY-MM-DD` of the newest of those days with a failure, or
            null.'
          title: Newer Failed At
        newer_failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on this engine on the days after `last_check`,
            where nothing answered. 0 when none failed.
          title: Newer Failed Checks
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Success score on the most recent day in the window on which
            this engine answered (0-100, 1 decimal place). Null when every check on
            it failed; `failed_checks` then counts them.
          title: Success Score
        success_score_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: '`success_score` minus its value on `first_check`, in points
            (1 decimal place). Null when the engine answered on one day only.'
          title: Success Score Delta
      title: OverviewEngineSummary
      type: object
    OverviewInsightBlock:
      additionalProperties: true
      description: 'The generated written summary of the project''s latest check batch.


        This is prose written by a language model over the project''s own numbers,

        not a measurement. The wording is not a stable contract and changes as the

        model and the prompt change; the shape of this block is what is stable.

        Null until the out-of-band pipeline has produced one, which is normal for

        a project whose first check batch has not finished.'
      properties:
        body:
          anyOf:
          - type: string
          - type: 'null'
          description: The summary in a short paragraph.
          title: Body
        date_span:
          anyOf:
          - $ref: '#/components/schemas/OverviewInsightDateSpan'
          - type: 'null'
          description: The days the insight was written over. It is the latest insight,
            so its days can differ from the request's window. Null when not recorded.
        generated_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp the insight was written.
          title: Generated At
        headline:
          anyOf:
          - type: string
          - type: 'null'
          description: One-line summary of the window.
          title: Headline
        key_highlight:
          anyOf:
          - type: string
          - type: 'null'
          description: The tone of the window as the writer judged it, e.g. `positive`,
            `neutral`.
          title: Key Highlight
        nudges:
          anyOf:
          - items:
              $ref: '#/components/schemas/OverviewInsightNudge'
            type: array
          - type: 'null'
          deprecated: true
          description: 'Deprecated: zero to three suggestions the writer generated
            beside the summary. They are raw output, not filtered by what you accepted
            or dismissed in the app, which shows its Action Center instead.'
          title: Nudges
        period:
          anyOf:
          - $ref: '#/components/schemas/OverviewInsightPeriod'
          - type: 'null'
          description: The checks the insight was written over. Null when not recorded.
      title: OverviewInsightBlock
      type: object
    OverviewInsightDateSpan:
      additionalProperties: true
      description: The days the insight was written over.
      properties:
        end:
          anyOf:
          - type: string
          - type: 'null'
          description: '`YYYY-MM-DD` of the last day.'
          title: End
        start:
          anyOf:
          - type: string
          - type: 'null'
          description: '`YYYY-MM-DD` of the first day.'
          title: Start
      title: OverviewInsightDateSpan
      type: object
    OverviewInsightNudge:
      additionalProperties: true
      description: One suggested action attached to the overview insight.
      properties:
        cited_batch_dates:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: ISO-8601 dates of the check batches this suggestion was drawn
            from.
          title: Cited Batch Dates
        severity:
          anyOf:
          - type: string
          - type: 'null'
          description: One of `info`, `warning` or `critical`.
          title: Severity
        text:
          anyOf:
          - type: string
          - type: 'null'
          description: The suggestion, as a sentence.
          title: Text
      title: OverviewInsightNudge
      type: object
    OverviewInsightPeriod:
      additionalProperties: true
      description: The checks the insight was written over.
      properties:
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Of those, the checks that failed and so informed none of its
            numbers.
          title: Failed Checks
        total_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engine checks in the period the insight describes.
          title: Total Checks
      title: OverviewInsightPeriod
      type: object
    OverviewKpis:
      additionalProperties: true
      description: 'The overview''s headline numbers: the KPI cards of the ziptie.ai
        Overview page.


        Computed by the same shared code as the app''s cards, from `trends.combined`,

        so every value here equals the card for the same window and filters. The

        rates are read from the most recent day in the window on which a check

        answered (`last_check`), and each delta is that day minus the first such

        day (`first_check`), so a delta describes the whole window rather than the

        last step.'
      properties:
        citations_count:
          description: Answers on `last_check` that cited a tracked URL.
          title: Citations Count
          type: integer
        citations_rate:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of answered checks citing a tracked URL on `last_check`
            (0-100).
          title: Citations Rate
        citations_rate_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: '`citations_rate` minus its value on `first_check`, in points.'
          title: Citations Rate Delta
        citations_total:
          description: 'Answered checks on `last_check` analysed for citations: the
            rate''s denominator.'
          title: Citations Total
          type: integer
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on `last_check`, left out of every rate. Null
            when no day answered or the data predates failed-check counts.
          title: Failed Checks
        failed_checks_in_range:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on every day of the window. Null when the data
            predates failed-check counts.
          title: Failed Checks In Range
        failed_only_checks:
          description: Failed checks on each of `failed_only_days`, index-aligned
            with it.
          items:
            type: integer
          title: Failed Only Checks
          type: array
        failed_only_days:
          description: '`YYYY-MM-DD` of each day in the window on which every check
            failed.'
          items:
            type: string
          title: Failed Only Days
          type: array
        first_check:
          anyOf:
          - type: string
          - type: 'null'
          description: '`YYYY-MM-DD` of the first day in the window on which a check
            answered: the day every delta is measured from.'
          title: First Check
        last_check:
          anyOf:
          - type: string
          - type: 'null'
          description: '`YYYY-MM-DD` of the most recent day in the window on which
            a check answered: the day every rate above is read from. Null when none
            answered.'
          title: Last Check
        mentions_count:
          description: Answers on `last_check` that named a tracked brand.
          title: Mentions Count
          type: integer
        mentions_rate:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of answered checks naming a tracked brand on `last_check`
            (0-100).
          title: Mentions Rate
        mentions_rate_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: '`mentions_rate` minus its value on `first_check`, in points.'
          title: Mentions Rate Delta
        mentions_total:
          description: 'Answered checks on `last_check`: the mention rate''s denominator.'
          title: Mentions Total
          type: integer
        newer_failed_at:
          anyOf:
          - type: string
          - type: 'null'
          description: '`YYYY-MM-DD` of the newest of those days with a failure. Null
            when there is none.'
          title: Newer Failed At
        newer_failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on the days after `last_check`, on each of which
            every check failed. 0 when none failed. Null as `failed_checks`.
          title: Newer Failed Checks
        sentiment_mixed:
          description: Answers on `last_check` classified mixed.
          title: Sentiment Mixed
          type: integer
        sentiment_negative:
          description: Answers on `last_check` classified negative.
          title: Sentiment Negative
          type: integer
        sentiment_positive:
          description: Answers on `last_check` classified positive.
          title: Sentiment Positive
          type: integer
        sentiment_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Sentiment score on `last_check` (0-100). Null when no answer
            that day carried a sentiment classification.
          title: Sentiment Score
        sentiment_score_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: '`sentiment_score` minus its value on `first_check`, in points.
            Null when either day has no sentiment.'
          title: Sentiment Score Delta
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Success score on `last_check` (0-100, 1 decimal place). Null
            when no check in the window answered.
          title: Success Score
        success_score_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: '`success_score` minus its value on `first_check`, in points
            (1 decimal place). Null with fewer than two answered days in the window.'
          title: Success Score Delta
      required:
      - success_score
      - success_score_delta
      - citations_rate
      - citations_rate_delta
      - citations_count
      - citations_total
      - mentions_rate
      - mentions_rate_delta
      - mentions_count
      - mentions_total
      - sentiment_score
      - sentiment_score_delta
      - sentiment_positive
      - sentiment_mixed
      - sentiment_negative
      - last_check
      - first_check
      - failed_checks
      - newer_failed_checks
      - newer_failed_at
      - failed_checks_in_range
      - failed_only_days
      - failed_only_checks
      title: OverviewKpis
      type: object
    OverviewRankSummary:
      additionalProperties: true
      description: Where one of your domains or brands stands in the full ranking.
      properties:
        name:
          description: Your domain or brand.
          title: Name
          type: string
        rank:
          anyOf:
          - type: integer
          - type: 'null'
          description: Its position by `count` over every ranked entity, the ones
            not in the top list included.
          title: Rank
        total:
          description: How many entities that ranking holds.
          title: Total
          type: integer
      required:
      - name
      - rank
      - total
      title: OverviewRankSummary
      type: object
    OverviewTopCompetitorEntry:
      additionalProperties: true
      description: One row of the Competitive Landscape lists.
      properties:
        color:
          description: Hex colour of the row in the ziptie.ai charts. Presentation
            only - it carries no measurement.
          title: Color
          type: string
        count:
          anyOf:
          - type: integer
          - type: number
          description: Citations or mentions in the window.
          title: Count
        is_user:
          description: True when the row is one of your project's domains or brands.
          title: Is User
          type: boolean
        name:
          description: The cited domain, or the canonical brand name.
          title: Name
          type: string
        percentage:
          description: 'The row''s share of the window''s citations or mentions (0-100,
            1 decimal place): the `share` of the same row on `competitors/citations`
            or `competitors/mentions`.'
          title: Percentage
          type: number
        rank:
          description: Position in this list, 1 for the largest `count`.
          title: Rank
          type: integer
      required:
      - name
      - count
      - percentage
      - is_user
      - color
      - rank
      title: OverviewTopCompetitorEntry
      type: object
    OverviewTopCompetitors:
      additionalProperties: true
      description: 'The Competitive Landscape: the most-cited domains and most-mentioned
        brands.


        Built by the same shared code as the app''s Overview widget, from the same

        rankings as `GET .../competitors/citations` and `.../competitors/mentions`,

        so a row''s count and share are the ones the Competitors page shows. Rows
        you

        hid in the app are left out, and every row is ordered by its count in the

        window.'
      properties:
        brands:
          description: The ten most-mentioned brands, largest first.
          items:
            $ref: '#/components/schemas/OverviewTopCompetitorEntry'
          title: Brands
          type: array
        domains:
          description: The ten most-cited domains, largest first.
          items:
            $ref: '#/components/schemas/OverviewTopCompetitorEntry'
          title: Domains
          type: array
        total_brands:
          description: How many brands the mentions ranking holds.
          title: Total Brands
          type: integer
        total_domains:
          description: How many domains the citations ranking holds.
          title: Total Domains
          type: integer
        user_brand_ranks:
          description: Each of your brands in the full mentions ranking, best-ranked
            first.
          items:
            $ref: '#/components/schemas/OverviewRankSummary'
          title: User Brand Ranks
          type: array
        user_domain_ranks:
          description: Each of your domains in the full citations ranking, best-ranked
            first.
          items:
            $ref: '#/components/schemas/OverviewRankSummary'
          title: User Domain Ranks
          type: array
      required:
      - domains
      - brands
      - user_domain_ranks
      - user_brand_ranks
      - total_domains
      - total_brands
      title: OverviewTopCompetitors
      type: object
    OverviewTrendPoint:
      additionalProperties: true
      description: One dated point on an engine's success-score trend line.
      properties:
        date:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 date of the check bucket.
          title: Date
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on that date, left out of every rate in `success_score`.
            On the `combined` series, the sum over the enabled engines. Null when
            not reported.
          title: Failed Checks
        success_score:
          anyOf:
          - $ref: '#/components/schemas/SuccessScoreBreakdown'
          - type: 'null'
          description: 'Score breakdown for that date, over the checks that answered.
            Null on a date with no answered check: either no check ran, or every check
            failed, in which case `failed_checks` says how many.'
      title: OverviewTrendPoint
      type: object
    OverviewTrendsBlock:
      additionalProperties: true
      description: Daily success-score trend lines, one series per engine.
      properties:
        bing_ai_overview:
          description: Bing AI Overview series.
          items:
            $ref: '#/components/schemas/OverviewTrendPoint'
          title: Bing Ai Overview
          type: array
        chatgpt:
          description: ChatGPT series.
          items:
            $ref: '#/components/schemas/OverviewTrendPoint'
          title: Chatgpt
          type: array
        combined:
          description: All enabled engines together.
          items:
            $ref: '#/components/schemas/OverviewTrendPoint'
          title: Combined
          type: array
        copilot:
          description: Microsoft Copilot series.
          items:
            $ref: '#/components/schemas/OverviewTrendPoint'
          title: Copilot
          type: array
        gemini:
          description: Gemini series.
          items:
            $ref: '#/components/schemas/OverviewTrendPoint'
          title: Gemini
          type: array
        google_ai_mode:
          description: Google AI Mode series.
          items:
            $ref: '#/components/schemas/OverviewTrendPoint'
          title: Google Ai Mode
          type: array
        google_aio:
          description: Google AI Overviews series.
          items:
            $ref: '#/components/schemas/OverviewTrendPoint'
          title: Google Aio
          type: array
        perplexity:
          description: Perplexity series.
          items:
            $ref: '#/components/schemas/OverviewTrendPoint'
          title: Perplexity
          type: array
      title: OverviewTrendsBlock
      type: object
    OverviewUncertainty:
      description: 'Likely ranges and change verdicts for the headline KPIs, a sibling
        of `kpis`.


        The ranges are likely ranges and the verdicts say whether a change is likely
        a real

        difference. Both are indicative outputs of the method named in `method`, not
        a

        validated test. Failed checks are excluded from every sample. The block describes
        the

        same two days as `kpis` (`latest_date` is `kpis.last_check`, `first_date`
        is

        `kpis.first_check`); when the days differ the whole block is null.'
      properties:
        changes:
          $ref: '#/components/schemas/UncertaintyKpiChanges'
          description: Verdicts on the combined KPI changes, `latest_date` against
            `first_date`.
        engines:
          additionalProperties:
            $ref: '#/components/schemas/EngineUncertainty'
          description: Per-engine ranges and verdicts, keyed by public engine name.
            An engine whose judged days differ from its `engine_summary` days has
            no key.
          title: Engines
          type: object
        failure_heavy_share:
          description: 'Share of an engine''s checks (0 to 1). When failed checks
            exceed it, a change is not judged: its verdict is `not_comparable` with
            reason `failure_heavy`.'
          title: Failure Heavy Share
          type: number
        first_date:
          description: ISO-8601 date (`YYYY-MM-DD`) of the first scored day, `kpis.first_check`.
          title: First Date
          type: string
        kpis:
          $ref: '#/components/schemas/UncertaintyKpiIntervals'
          description: Likely ranges for the combined headline KPIs on `latest_date`.
        latest_date:
          description: ISO-8601 date (`YYYY-MM-DD`) of the latest scored day, `kpis.last_check`.
          title: Latest Date
          type: string
        level:
          description: The method's nominal level for the likely ranges, 0 to 1. It
            is a setting of an indicative method, not a validated or guaranteed coverage.
          title: Level
          type: number
        method:
          description: Name of the method that produced the ranges and verdicts.
          title: Method
          type: string
        method_version:
          description: Version of the method. Ranges and verdicts may shift when it
            changes, without a contract version bump; changes are announced in the
            changelog.
          title: Method Version
          type: integer
        min_answers:
          description: Fewest answers a rate needs before a range is given.
          title: Min Answers
          type: integer
        min_engine_sentiment_answers:
          description: Fewest answers with sentiment an engine needs before a sentiment
            range.
          title: Min Engine Sentiment Answers
          type: integer
        min_prompts:
          description: Fewest distinct prompts a range or verdict needs.
          title: Min Prompts
          type: integer
        settles_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC time after which no day in the comparison can
            change on retry. Before it, a day inside the retry window can still change
            and verdicts carry the `retries_pending` caveat. Null when not known.
          title: Settles At
      required:
      - method
      - method_version
      - level
      - min_answers
      - min_prompts
      - min_engine_sentiment_answers
      - failure_heavy_share
      - latest_date
      - first_date
      - kpis
      - changes
      title: OverviewUncertainty
      type: object
    PatchBrandFactBody:
      additionalProperties: false
      properties:
        category:
          anyOf:
          - enum:
            - price
            - plan
            - feature
            - location
            - other
            type: string
          - type: 'null'
          description: If set, the new category. A changed category writes a new version.
          title: Category
        statement:
          anyOf:
          - maxLength: 200
            minLength: 1
            type: string
          - type: 'null'
          description: If set, the new statement. A changed statement writes a new
            version.
          title: Statement
        status:
          anyOf:
          - enum:
            - active
            - paused
            type: string
          - type: 'null'
          description: If set, `paused` stops checking answers against the fact and
            `active` resumes. A status change alone does not write a version.
          title: Status
      title: PatchBrandFactBody
      type: object
    PatchCompetitorTagBody:
      additionalProperties: false
      properties:
        color:
          anyOf:
          - maxLength: 7
            minLength: 7
            type: string
          - type: 'null'
          description: If set, the new hex colour '#RRGGBB'.
          title: Color
        name:
          anyOf:
          - maxLength: 120
            minLength: 1
            type: string
          - type: 'null'
          description: If set, the new display name.
          title: Name
      title: PatchCompetitorTagBody
      type: object
    PatchPromptBody:
      additionalProperties: false
      properties:
        monitoring_status:
          anyOf:
          - type: boolean
          - type: 'null'
          description: If set, update the prompt's monitoring state.
          title: Monitoring Status
        tag_ids:
          anyOf:
          - items:
              maximum: 2147483647
              minimum: 1
              type: integer
            maxItems: 100
            type: array
          - type: 'null'
          description: If set, replace the prompt's tag associations with this list.
            At most 100, each 1-2147483647.
          title: Tag Ids
        url_ids:
          anyOf:
          - items:
              maximum: 2147483647
              minimum: 1
              type: integer
            type: array
          - type: 'null'
          description: If set, replace the prompt's URL associations with this list,
            each 1-2147483647.
          title: Url Ids
      title: PatchPromptBody
      type: object
    PatchTagBody:
      additionalProperties: false
      properties:
        color:
          anyOf:
          - maxLength: 7
            minLength: 7
            type: string
          - type: 'null'
          description: If set, update the tag's hex color.
          title: Color
        name:
          anyOf:
          - maxLength: 50
            minLength: 1
            type: string
          - type: 'null'
          description: If set, update the tag's display name.
          title: Name
      title: PatchTagBody
      type: object
    PatchUrlBody:
      additionalProperties: false
      properties:
        url:
          anyOf:
          - maxLength: 1024
            minLength: 1
            type: string
          - type: 'null'
          description: If set, update the URL's value. Up to 1024 characters.
          title: Url
      title: PatchUrlBody
      type: object
    ProjectCheckListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: Checks on this page, newest first, across every prompt in the
            project.
          items:
            $ref: '#/components/schemas/ProjectCheckSummary'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      title: ProjectCheckListResponse
      type: object
    ProjectCheckSummary:
      additionalProperties: true
      description: 'A check summary carrying the prompt it belongs to.


        Extends `CheckSummary` rather than restating it, so the project-wide and

        per-prompt check surfaces cannot drift apart field by field. `prompt_id` is

        the only addition, and it is not optional detail: since June 2026 a

        `check_no` is one project check cycle, so every prompt answered in that

        check shares it, and before then each prompt numbered its own checks, so two

        rows could share one without being related. Either way a consumer cannot

        join a row back to its prompt without `prompt_id`.'
      properties:
        answered_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engines whose run of this check answered, counted over the
            engines `success_score` averages, `bing_ai_overview` included. A `google_aio`
            or `bing_ai_overview` run that showed no overview neither answered nor
            failed. 0 when none answered, in which case `success_score` is null. Null
            when the count is not reported.
          title: Answered Checks
        check_no:
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count.
          title: Check No
          type: integer
        engines:
          anyOf:
          - additionalProperties:
              $ref: '#/components/schemas/CheckEngineOutcome'
            type: object
          - type: 'null'
          description: Per-engine outcome for this check, keyed by public engine name.
            Only engines with a row for the check appear, a failed one included with
            `failed` true. See the check detail endpoint for the answer text, sentiment,
            brands and cited sources behind these flags.
          title: Engines
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engines whose run of this check failed, counted over the same
            engines as `answered_checks`. A failed run returned no answer, so it is
            left out of `success_score` rather than scored as 0. Null when the count
            is not reported.
          title: Failed Checks
        prompt_id:
          description: Identifier of the prompt this check belongs to.
          title: Prompt Id
          type: integer
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Cross-engine score for this check (0-1). Failed checks are
            left out, and so is a `google_aio` or `bing_ai_overview` run that showed
            no overview. When no engine answered this check, `success_score` is null
            and `answered_checks` is 0. `answered_checks` and `failed_checks` count
            over the same engines this score averages.
          title: Success Score
        timestamp:
          description: ISO-8601 UTC timestamp the check was recorded.
          title: Timestamp
          type: string
      required:
      - check_no
      - timestamp
      - prompt_id
      title: ProjectCheckSummary
      type: object
    ProjectDetailResponse:
      additionalProperties: true
      description: 'The project an API token is bound to, as configuration plus freshness.


        Deliberately omits the workspace identifier: the public surface carries

        operations on the data ziptie.ai gathers, not a subscription management

        surface. ``write_access`` is published because it is what tells a consumer,

        before it writes, whether every write will be refused with 403

        ``SUBSCRIPTION_READ_ONLY``. ``enabled_engines`` is published even though it

        is plan-derived, because it is what explains an engine having no rows

        anywhere else on the surface.


        It is a permission rather than a schedule, so it is published alongside

        ``monitored_prompt_count``. Upstream gates every scheduling path on a

        prompt being switched on as well as on the plan being live, and with the

        engine roster as the only signal a fully paused project on a paid plan read

        as healthy monitoring forever.'
      properties:
        brands:
          description: Brands the project tracks as its own. Empty when none are configured.
          items:
            type: string
          title: Brands
          type: array
        check_frequency_days:
          description: Days between scheduled check cycles for this project.
          title: Check Frequency Days
          type: integer
        country:
          description: Country the checks are run for, as an ISO-3166 alpha-2 code.
          title: Country
          type: string
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of project creation.
          title: Created At
        current_cycle:
          description: Sequence number of the latest check cycle. `0` before the first
            cycle runs.
          title: Current Cycle
          type: integer
        device:
          description: 'Device profile the checks are run on: `desktop` or `mobile`.'
          title: Device
          type: string
        domains:
          description: Domains connected to the project. Empty when none are connected.
          items:
            type: string
          title: Domains
          type: array
        enabled_engines:
          anyOf:
          - $ref: '#/components/schemas/ProjectEnabledEngines'
          - type: 'null'
          description: 'AI engines the workspace plan allows to be checked for this
            project, keyed by public engine name. Derived from the workspace plan
            rather than from a per-project setting, so it can change without any change
            to the project and without a new check: do not cache it as stable project
            configuration. `null` when the plan allows no engine at all, which is
            what a lapsed subscription, or one whose period has not started yet, looks
            like from the outside. It is a permission, not a schedule: whether any
            check actually runs also depends on `monitored_prompt_count`, and none
            runs while that is `0`.'
        first_check_complete:
          description: Whether the project's first check cycle has finished.
          title: First Check Complete
          type: boolean
        gsc_property:
          anyOf:
          - type: string
          - type: 'null'
          description: Connected Google Search Console property URL, or `null` when
            no property is connected.
          title: Gsc Property
        id:
          description: Unique identifier of the project.
          title: Id
          type: string
        language:
          description: Language the checks are run in, as an ISO-639-1 code.
          title: Language
          type: string
        last_check_date:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the most recent project-level check
            cycle. `null` when no cycle has run yet.
          title: Last Check Date
        monitored_prompt_count:
          description: 'Number of prompts in the project that are switched on for
            monitoring, a subset of `prompt_count`. Scheduling is gated on it: while
            it is `0` no check runs for the project whatever `enabled_engines` lists,
            and `last_check_date` and `current_cycle` do not move.'
          title: Monitored Prompt Count
          type: integer
        name:
          description: Display name of the project.
          title: Name
          type: string
        prompt_count:
          description: Number of prompts tracked in the project, paused ones included.
            See `monitored_prompt_count` for the subset that is scheduled.
          title: Prompt Count
          type: integer
        url_count:
          description: Number of URLs tracked in the project.
          title: Url Count
          type: integer
        write_access:
          description: 'Whether the workspace subscription grants write access now.
            `false` means read-only: every read keeps working and every write (POST/PATCH/DELETE
            and the matching MCP tools) is refused with 403 `SUBSCRIPTION_READ_ONLY`
            until renewed. A dunning, cancel-at-period-end or payment-confirmation
            workspace still inside its paid period is `true`. Independent of token
            scope.'
          title: Write Access
          type: boolean
      required:
      - id
      - name
      - domains
      - brands
      - country
      - language
      - device
      - check_frequency_days
      - current_cycle
      - first_check_complete
      - prompt_count
      - monitored_prompt_count
      - url_count
      - write_access
      title: ProjectDetailResponse
      type: object
    ProjectEnabledEngines:
      additionalProperties: true
      description: 'Per-engine monitoring flags, keyed by public engine name.


        The upstream roster uses internal keys (`gpt`, `aio`, `ai_mode`); the

        router translates them through ``rewrite_engine_keys`` before this model

        is built, so no internal key ever reaches a public body.'
      properties:
        bing_ai_overview:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether Bing AI Overview is checked for this project.
          title: Bing Ai Overview
        chatgpt:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether ChatGPT is checked for this project.
          title: Chatgpt
        copilot:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether Microsoft Copilot is checked for this project.
          title: Copilot
        gemini:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether Gemini is checked for this project.
          title: Gemini
        google_ai_mode:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether Google AI Mode is checked for this project.
          title: Google Ai Mode
        google_aio:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether Google AI Overviews is checked for this project.
          title: Google Aio
        perplexity:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether Perplexity is checked for this project.
          title: Perplexity
      title: ProjectEnabledEngines
      type: object
    ProjectListItem:
      additionalProperties: true
      description: 'One project the calling token reaches, as configuration and freshness.


        The subset of ``ProjectDetailResponse`` that is cheap to read for many

        projects at once. Counts, domains, brands, the GSC property and

        ``write_access`` stay on the detail read. Like it, no workspace identifier.'
      properties:
        check_frequency_days:
          description: Days between scheduled check cycles for this project.
          title: Check Frequency Days
          type: integer
        country:
          description: Country the checks are run for, as an ISO-3166 alpha-2 code.
          title: Country
          type: string
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of project creation.
          title: Created At
        enabled_engines:
          anyOf:
          - $ref: '#/components/schemas/ProjectEnabledEngines'
          - type: 'null'
          description: 'AI engines the workspace plan allows to be checked for this
            project, keyed by public engine name. Derived from the workspace plan
            rather than from a per-project setting, so it can change without any change
            to the project and without a new check: do not cache it as stable project
            configuration. `null` when the plan allows no engine at all, which is
            what a lapsed subscription, or one whose period has not started yet, looks
            like from the outside. It is a permission, not a schedule: whether any
            check actually runs also depends on `monitored_prompt_count`, and none
            runs while that is `0`.'
        first_check_complete:
          description: Whether the project's first check cycle has finished.
          title: First Check Complete
          type: boolean
        id:
          description: Unique identifier of the project.
          title: Id
          type: string
        language:
          description: Language the checks are run in, as an ISO-639-1 code.
          title: Language
          type: string
        last_check_date:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the most recent project-level check
            cycle. `null` when no cycle has run yet.
          title: Last Check Date
        name:
          description: Display name of the project.
          title: Name
          type: string
      required:
      - id
      - name
      - country
      - language
      - check_frequency_days
      - first_check_complete
      title: ProjectListItem
      type: object
    ProjectListResponse:
      additionalProperties: true
      properties:
        data:
          description: Projects on this page.
          items:
            $ref: '#/components/schemas/ProjectListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: ProjectListResponse
      type: object
    ProjectThresholdsBlock:
      additionalProperties: true
      description: 'The project''s good/fair display bands for the dashboard''s headline
        metrics.


        These are presentation bands, not inputs to any published number. Nothing

        on this surface is computed from them: `success_score` is the mean of the

        per-engine scores for a check, and the bands do not change it. The score

        is measured on the project''s own prompts, engines, country and cadence, so

        compare it with the project''s own past, never with another project''s. A

        value at or above `*_target_good` is "good", at or above

        `*_target_fair` is "fair", below that is "poor". All eight are

        percentages on a 0-100 scale, and the defaults shown apply when the

        project has never customised them.'
      properties:
        citations_target_fair:
          default: 15
          description: Citation rate at or above which the project reads as fair.
          title: Citations Target Fair
          type: integer
        citations_target_good:
          default: 30
          description: Citation rate at or above which the project reads as good.
          title: Citations Target Good
          type: integer
        mentions_target_fair:
          default: 15
          description: Mention rate at or above which the project reads as fair.
          title: Mentions Target Fair
          type: integer
        mentions_target_good:
          default: 30
          description: Mention rate at or above which the project reads as good.
          title: Mentions Target Good
          type: integer
        score_target_fair:
          default: 40
          description: AI success score at or above which the project reads as fair.
          title: Score Target Fair
          type: integer
        score_target_good:
          default: 70
          description: AI success score at or above which the project reads as good.
          title: Score Target Good
          type: integer
        sentiment_target_fair:
          default: 40
          description: Sentiment score at or above which the project reads as fair.
          title: Sentiment Target Fair
          type: integer
        sentiment_target_good:
          default: 70
          description: Sentiment score at or above which the project reads as good.
          title: Sentiment Target Good
          type: integer
      title: ProjectThresholdsBlock
      type: object
    PromptCheckSummary:
      additionalProperties: true
      properties:
        answered_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engines whose run of this check answered, counted over the
            engines `success_score` averages, `bing_ai_overview` included. A `google_aio`
            or `bing_ai_overview` run that showed no overview neither answered nor
            failed. 0 when none answered, in which case `success_score` is null. Null
            when the count is not reported.
          title: Answered Checks
        check_no:
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count.
          title: Check No
          type: integer
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Engines whose run of this check failed, counted over the same
            engines as `answered_checks`. A failed run returned no answer, so it is
            left out of `success_score` rather than scored as 0. Null when the count
            is not reported.
          title: Failed Checks
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Cross-engine score for this check (0-1). Failed checks are
            left out, and so is a `google_aio` or `bing_ai_overview` run that showed
            no overview. When no engine answered this check, `success_score` is null
            and `answered_checks` is 0. `answered_checks` and `failed_checks` count
            over the same engines this score averages.
          title: Success Score
        timestamp:
          description: ISO-8601 UTC timestamp the check was recorded.
          title: Timestamp
          type: string
      required:
      - check_no
      - timestamp
      - success_score
      title: PromptCheckSummary
      type: object
    PromptCreateResponse:
      additionalProperties: true
      properties:
        checks_start_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC instant before which ZipTie does not check this
            prompt, or `null` when checks are not deferred. A create or resume into
            a cadence whose slots are held only until that cadence's window resets
            succeeds and reports the reset here; the prompt is checked from this instant.
            Only a future instant is reported.
          title: Checks Start At
        id:
          description: Newly assigned prompt identifier.
          title: Id
          type: integer
        monitoring_frequency:
          anyOf:
          - type: integer
          - type: 'null'
          description: Monitoring cadence stamped on the prompt, in days. Taken from
            the project's `check_frequency_days`; `null` when the prompt is not monitored.
          title: Monitoring Frequency
        monitoring_status:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether the prompt is currently monitored.
          title: Monitoring Status
        text:
          anyOf:
          - type: string
          - type: 'null'
          description: The created prompt's text.
          title: Text
      required:
      - id
      title: PromptCreateResponse
      type: object
    PromptDetailResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        checks:
          anyOf:
          - items:
              $ref: '#/components/schemas/PromptCheckSummary'
            type: array
          - type: 'null'
          description: Per-check success scores for the checks in the applied window,
            oldest first. A check is in the window when one of its engine answers
            was recorded on a day inside it (UTC), as on the check listings.
          title: Checks
        checks_start_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC instant before which ZipTie does not check this
            prompt, or `null` when checks are not deferred. A create or resume into
            a cadence whose slots are held only until that cadence's window resets
            succeeds and reports the reset here; the prompt is checked from this instant.
            Only a future instant is reported.
          title: Checks Start At
        funnel_stage:
          anyOf:
          - enum:
            - awareness
            - consideration
            - decision
            type: string
          - type: 'null'
          description: 'The buyer-journey stage the prompt represents: `awareness`,
            `consideration` or `decision`. Assigned and changed the same way as `intent`,
            and `null` while the prompt is unclassified. Set independently of `intent`,
            so one can be `null` while the other has a value.'
          title: Funnel Stage
        id:
          description: Prompt identifier.
          title: Id
          type: integer
        intent:
          anyOf:
          - enum:
            - informational
            - commercial
            - transactional
            - navigational
            type: string
          - type: 'null'
          description: 'What the person asking the prompt wants: `informational` (to
            learn about a topic), `commercial` (to compare options before buying),
            `transactional` (to buy or act now) or `navigational` (to reach a specific
            brand or site). Assigned automatically shortly after the prompt is added;
            a member with edit access can change it in the app. `null` while the prompt
            is unclassified, so a prompt created a moment ago reads `null` at first.
            Set independently of `funnel_stage`. Not settable or filterable through
            this API.'
          title: Intent
        last_check_date:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the most recent check.
          title: Last Check Date
        monitoring_frequency:
          anyOf:
          - type: integer
          - type: 'null'
          description: 'Monitoring cadence in days: 1 daily, 7 weekly, 30 monthly.
            This is the prompt''s project `check_frequency_days`, so the two never
            disagree, and changing a project''s cadence moves every prompt in it.
            `null` when the prompt is not monitored.'
          title: Monitoring Frequency
        monitoring_status:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether the prompt is actively monitored.
          title: Monitoring Status
        tags:
          anyOf:
          - items:
              $ref: '#/components/schemas/PromptTag'
            type: array
          - type: 'null'
          description: Tags associated with the prompt.
          title: Tags
        text:
          description: The prompt's tracked query text.
          title: Text
          type: string
        urls:
          anyOf:
          - items:
              $ref: '#/components/schemas/PromptUrl'
            type: array
          - type: 'null'
          description: URLs linked to the prompt.
          title: Urls
      required:
      - applied
      - id
      - text
      title: PromptDetailResponse
      type: object
    PromptListItem:
      additionalProperties: true
      properties:
        bing_organic_position:
          anyOf:
          - type: integer
          - type: 'null'
          description: Best Bing organic rank (1 is the top result) a URL from the
            project's domain held at the prompt's most recent check. Bing organic
            results are captured by `bing_ai_overview` checks. `null` when the domain
            did not rank in the top results, the prompt has not been checked, or that
            check has no Bing organic data.
          title: Bing Organic Position
        checks_start_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC instant before which ZipTie does not check this
            prompt, or `null` when checks are not deferred. A create or resume into
            a cadence whose slots are held only until that cadence's window resets
            succeeds and reports the reset here; the prompt is checked from this instant.
            Only a future instant is reported.
          title: Checks Start At
        funnel_stage:
          anyOf:
          - enum:
            - awareness
            - consideration
            - decision
            type: string
          - type: 'null'
          description: 'The buyer-journey stage the prompt represents: `awareness`,
            `consideration` or `decision`. Assigned and changed the same way as `intent`,
            and `null` while the prompt is unclassified. Set independently of `intent`,
            so one can be `null` while the other has a value.'
          title: Funnel Stage
        google_organic_position:
          anyOf:
          - type: integer
          - type: 'null'
          description: Best Google organic rank (1 is the top result) a URL from the
            project's domain held at the prompt's most recent check. Google organic
            results are captured by `google_aio` checks. `null` when the domain did
            not rank in the top results, the prompt has not been checked, or that
            check has no Google organic data.
          title: Google Organic Position
        id:
          description: Prompt identifier.
          title: Id
          type: integer
        intent:
          anyOf:
          - enum:
            - informational
            - commercial
            - transactional
            - navigational
            type: string
          - type: 'null'
          description: 'What the person asking the prompt wants: `informational` (to
            learn about a topic), `commercial` (to compare options before buying),
            `transactional` (to buy or act now) or `navigational` (to reach a specific
            brand or site). Assigned automatically shortly after the prompt is added;
            a member with edit access can change it in the app. `null` while the prompt
            is unclassified, so a prompt created a moment ago reads `null` at first.
            Set independently of `funnel_stage`. Not settable or filterable through
            this API.'
          title: Intent
        last_check_date:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the most recent check.
          title: Last Check Date
        monitoring_frequency:
          anyOf:
          - type: integer
          - type: 'null'
          description: 'Monitoring cadence in days: 1 daily, 7 weekly, 30 monthly.
            This is the prompt''s project `check_frequency_days`, so the two never
            disagree, and changing a project''s cadence moves every prompt in it.
            `null` when the prompt is not monitored.'
          title: Monitoring Frequency
        monitoring_status:
          description: Whether the prompt is actively monitored.
          title: Monitoring Status
          type: boolean
        tags:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Tag names associated with the prompt.
          title: Tags
        text:
          description: The prompt's tracked query text.
          title: Text
          type: string
        times_checked:
          anyOf:
          - type: integer
          - type: 'null'
          description: Lifetime count of check runs recorded for this prompt.
          title: Times Checked
      required:
      - id
      - text
      - monitoring_status
      title: PromptListItem
      type: object
    PromptListResponse:
      additionalProperties: true
      properties:
        data:
          description: Prompts on this page.
          items:
            $ref: '#/components/schemas/PromptListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: PromptListResponse
      type: object
    PromptPerformanceEngine:
      description: One engine's answer at the check a prompt-performance row reads.
      properties:
        cited:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether the answer cited the project's domain. `null` when
            the engine failed or has no row at that check.
          title: Cited
        failed:
          anyOf:
          - type: boolean
          - type: 'null'
          description: '`true` when the engine''s run at that check failed (its answer
            fields are then null), `false` when it answered, `null` when the engine
            has no row at that check.'
          title: Failed
        favored_brand:
          anyOf:
          - type: string
          - type: 'null'
          description: The brand the answer favoured most, or `null` when none was
            identified.
          title: Favored Brand
        mentioned:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether the answer mentioned the project's brand. `null` when
            the engine failed or has no row at that check.
          title: Mentioned
        response_status:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether the search engine showed an AI overview for the prompt
            at that check. Reported for `google_aio`, and for `bing_ai_overview` where
            available; `null` on every other engine.
          title: Response Status
        sentiment:
          anyOf:
          - enum:
            - Positive
            - Mixed
            - Negative
            - Unknown
            type: string
          - type: 'null'
          description: 'The answer''s sentiment toward the brand: `Positive`, `Mixed`,
            `Negative`, or `Unknown` when the answer carried no sentiment read. `null`
            unless `failed` is `false`.'
          title: Sentiment
      required:
      - cited
      - mentioned
      - sentiment
      - favored_brand
      - failed
      - response_status
      title: PromptPerformanceEngine
      type: object
    PromptPerformanceItem:
      additionalProperties: true
      properties:
        check_no:
          anyOf:
          - type: integer
          - type: 'null'
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count. `null` when the row reads no check.
          title: Check No
        engines:
          additionalProperties:
            $ref: '#/components/schemas/PromptPerformanceEngine'
          description: The answer per engine at the check `success_score` reads, keyed
            by public engine name, with exactly one entry for each engine in `applied.engines`.
          title: Engines
          type: object
        id:
          description: Prompt identifier.
          title: Id
          type: integer
        last_check_date:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC instant the check the row reads was first answered.
            `null` for a paused prompt with no check on or before `date_to`.
          title: Last Check Date
        monitoring_status:
          description: Whether the prompt is actively monitored.
          title: Monitoring Status
          type: boolean
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: 'The prompt''s AI Score at the latest check it had on or before
            `date_to`, 0-100 at one decimal: the mean over the engines in `applied.engines`
            that answered that check. `null` when none answered.'
          title: Success Score
        success_score_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: 'The score change over the window: the last measured point
            of `success_score_trend` minus the first in the window, in points at one
            decimal. `null` with fewer than two measured points.'
          title: Success Score Delta
        success_score_trend:
          description: 'One value per `check_dates` entry, index-aligned: the mean
            of the scores of the engines that scored the prompt that day, 0-100 at
            one decimal. `null` on a day the prompt has no check or no engine scored.'
          items:
            anyOf:
            - type: number
            - type: 'null'
          title: Success Score Trend
          type: array
        tags:
          description: Every tag the prompt carries. `tag_ids` chooses the rows and
            does not narrow this list.
          items:
            $ref: '#/components/schemas/PromptPerformanceTag'
          title: Tags
          type: array
        text:
          description: The prompt's tracked query text.
          title: Text
          type: string
      required:
      - id
      - text
      - monitoring_status
      - tags
      - success_score
      - success_score_delta
      - success_score_trend
      - engines
      - last_check_date
      - check_no
      title: PromptPerformanceItem
      type: object
    PromptPerformanceResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        check_dates:
          description: 'The check days in the window, `YYYY-MM-DD`, oldest first:
            the axis every `success_score_trend` is index-aligned with.'
          items:
            type: string
          title: Check Dates
          type: array
        data:
          description: Prompts on this page.
          items:
            $ref: '#/components/schemas/PromptPerformanceItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - check_dates
      title: PromptPerformanceResponse
      type: object
    PromptPerformanceTag:
      additionalProperties: true
      properties:
        color:
          anyOf:
          - type: string
          - type: 'null'
          description: Hex color used in the app for this tag (#RRGGBB).
          title: Color
        id:
          description: Tag identifier.
          title: Id
          type: integer
        name:
          description: Display name of the tag.
          title: Name
          type: string
      required:
      - id
      - name
      title: PromptPerformanceTag
      type: object
    PromptRef:
      additionalProperties: true
      properties:
        id:
          description: Prompt identifier.
          title: Id
          type: integer
        success_score:
          description: The prompt's latest AI Score in the window, 0-100 at one decimal.
          title: Success Score
          type: number
        text:
          description: The prompt's tracked query text.
          title: Text
          type: string
      required:
      - id
      - text
      - success_score
      title: PromptRef
      type: object
    PromptSuggestionListItem:
      additionalProperties: true
      properties:
        cluster_name:
          anyOf:
          - type: string
          - type: 'null'
          description: Owning cluster name (denormalized for convenience).
          title: Cluster Name
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp.
          title: Created At
        evidence_topic_ids:
          description: Topics the agent cited as evidence for this suggestion.
          items:
            type: integer
          title: Evidence Topic Ids
          type: array
        id_cluster:
          description: Owning cluster identifier.
          title: Id Cluster
          type: integer
        id_prompt_suggestion:
          description: Suggestion identifier.
          title: Id Prompt Suggestion
          type: integer
        suggested_text:
          description: The AI-generated suggested prompt text.
          title: Suggested Text
          type: string
      required:
      - id_prompt_suggestion
      - id_cluster
      - suggested_text
      title: PromptSuggestionListItem
      type: object
    PromptSuggestionListResponse:
      additionalProperties: true
      properties:
        data:
          description: Pending suggestions on this page.
          items:
            $ref: '#/components/schemas/PromptSuggestionListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: PromptSuggestionListResponse
      type: object
    PromptTag:
      additionalProperties: true
      properties:
        color:
          description: Hex color used in the UI for this tag (#RRGGBB).
          title: Color
          type: string
        id_tag:
          description: Tag identifier.
          title: Id Tag
          type: integer
        name:
          description: Display name of the tag.
          title: Name
          type: string
      required:
      - id_tag
      - name
      - color
      title: PromptTag
      type: object
    PromptUpdateResponse:
      additionalProperties: true
      properties:
        checks_start_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC instant before which ZipTie does not check this
            prompt, or `null` when checks are not deferred. A create or resume into
            a cadence whose slots are held only until that cadence's window resets
            succeeds and reports the reset here; the prompt is checked from this instant.
            Only a future instant is reported.
          title: Checks Start At
        funnel_stage:
          anyOf:
          - enum:
            - awareness
            - consideration
            - decision
            type: string
          - type: 'null'
          description: 'The buyer-journey stage the prompt represents: `awareness`,
            `consideration` or `decision`. Assigned and changed the same way as `intent`,
            and `null` while the prompt is unclassified. Set independently of `intent`,
            so one can be `null` while the other has a value.'
          title: Funnel Stage
        id:
          anyOf:
          - type: integer
          - type: 'null'
          description: Prompt identifier.
          title: Id
        intent:
          anyOf:
          - enum:
            - informational
            - commercial
            - transactional
            - navigational
            type: string
          - type: 'null'
          description: 'What the person asking the prompt wants: `informational` (to
            learn about a topic), `commercial` (to compare options before buying),
            `transactional` (to buy or act now) or `navigational` (to reach a specific
            brand or site). Assigned automatically shortly after the prompt is added;
            a member with edit access can change it in the app. `null` while the prompt
            is unclassified, so a prompt created a moment ago reads `null` at first.
            Set independently of `funnel_stage`. Not settable or filterable through
            this API.'
          title: Intent
        monitoring_status:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Current monitoring flag after the update.
          title: Monitoring Status
        text:
          anyOf:
          - type: string
          - type: 'null'
          description: Current prompt text after the update.
          title: Text
      title: PromptUpdateResponse
      type: object
    PromptUrl:
      additionalProperties: true
      properties:
        id_lite_url:
          description: URL identifier.
          title: Id Lite Url
          type: integer
        url:
          description: The tracked URL.
          title: Url
          type: string
      required:
      - id_lite_url
      - url
      title: PromptUrl
      type: object
    PromptsSummaryResponse:
      additionalProperties: true
      properties:
        active_count:
          description: Monitored prompts among `total_count`.
          title: Active Count
          type: integer
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        average_success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: The latest measured point of `average_success_score_trend`,
            0-100 at one decimal. `null` when no active prompt was measured in the
            window.
          title: Average Success Score
        average_success_score_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: 'The change over the window: the last measured point of `average_success_score_trend`
            minus the first in the window, in points at one decimal. `null` with fewer
            than two measured points.'
          title: Average Success Score Delta
        average_success_score_trend:
          description: 'One value per `check_dates` entry, index-aligned: the mean
            AI Score of the active prompts measured that day, 0-100 at one decimal.
            `null` on a day no active prompt was measured.'
          items:
            anyOf:
            - type: number
            - type: 'null'
          title: Average Success Score Trend
          type: array
        best_prompt:
          anyOf:
          - $ref: '#/components/schemas/PromptRef'
          - type: 'null'
          description: The active prompt with the highest latest score, or `null`
            when none scored.
        check_dates:
          description: The check days in the window, `YYYY-MM-DD`, oldest first.
          items:
            type: string
          title: Check Dates
          type: array
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed engine checks of the active prompts the filters keep,
            on the day `average_success_score` shows. With no measured day, the engines
            whose latest check failed, summed over those prompts.
          title: Failed Checks
        newer_failed_at:
          anyOf:
          - type: string
          - type: 'null'
          description: The newest of those later days with a failure, `YYYY-MM-DD`,
            or `null` when there is none.
          title: Newer Failed At
        newer_failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed engine checks of those prompts on the days after the
            one `average_success_score` shows, where none of them scored; 0 when none
            failed there. `null` when no day was measured.
          title: Newer Failed Checks
        paused_count:
          description: Paused prompts among `total_count`.
          title: Paused Count
          type: integer
        total_count:
          description: Prompts the filters keep, active and paused.
          title: Total Count
          type: integer
        worst_prompt:
          anyOf:
          - $ref: '#/components/schemas/PromptRef'
          - type: 'null'
          description: The active prompt with the lowest latest score, set only when
            that score is below 50 and the prompt is not `best_prompt`; otherwise
            `null`.
      required:
      - applied
      - total_count
      - active_count
      - paused_count
      - average_success_score
      - average_success_score_delta
      - average_success_score_trend
      - check_dates
      - failed_checks
      - newer_failed_checks
      - newer_failed_at
      - best_prompt
      - worst_prompt
      title: PromptsSummaryResponse
      type: object
    RateLimitsBlock:
      additionalProperties: true
      properties:
        per_day:
          anyOf:
          - type: integer
          - type: 'null'
          description: Per-token daily request quota.
          title: Per Day
        per_minute:
          anyOf:
          - type: integer
          - type: 'null'
          description: Per-token requests-per-minute cap.
          title: Per Minute
        workspace_per_minute:
          anyOf:
          - type: integer
          - type: 'null'
          description: Workspace-wide per-minute cap (sum across all tokens).
          title: Workspace Per Minute
      title: RateLimitsBlock
      type: object
    SerpCompetitorListItem:
      additionalProperties: true
      properties:
        count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Number of top-10 organic SERP appearances in the window.
          title: Count
        count_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: '`count` over the second half of the window minus the first
            half. Null when the first half has no data.'
          title: Count Delta
        is_competitor:
          default: false
          description: True when the row carries a competitor mark in the app and
            is not hidden. Always false on tag rows and with `group_by=category`.
          title: Is Competitor
          type: boolean
        is_hidden:
          default: false
          description: True when the row is hidden in the app. Hidden rows appear
            only with `filter=hidden`.
          title: Is Hidden
          type: boolean
        is_user:
          default: false
          description: True on the project's own domains.
          title: Is User
          type: boolean
        last_check_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Top-10 SERP slots held in the most recent check.
          title: Last Check Count
        last_check_share:
          anyOf:
          - type: number
          - type: 'null'
          description: Share (%) of top-10 SERP slots held in the most recent check.
          title: Last Check Share
        last_check_share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: Change in `last_check_share` against the previous check, in
            percentage points.
          title: Last Check Share Delta
        name:
          description: Competitor domain name.
          title: Name
          type: string
        rank:
          anyOf:
          - type: integer
          - type: 'null'
          description: 'Position by the most recent check''s count over every ranked
            entity, hidden ones included, as the app''s # column shows it. Not re-based
            by `filter`, `competitor_tag_ids` or paging, so the rows of a filtered
            page can skip numbers. Sort by `rank:asc` for the app''s order.'
          title: Rank
        rank_delta:
          anyOf:
          - type: integer
          - type: 'null'
          description: Change in rank between the two latest checks (positive = improved).
          title: Rank Delta
        share:
          anyOf:
          - type: number
          - type: 'null'
          description: Share (%) of total top-10 SERP slots held by this domain.
          title: Share
        share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: Change in `share` between the two halves of the window, in
            percentage points.
          title: Share Delta
        tags:
          description: Competitor tags the domain carries, in name order. Empty on
            hidden rows.
          items:
            $ref: '#/components/schemas/CompetitorTagRef'
          title: Tags
          type: array
        top3_share:
          anyOf:
          - type: number
          - type: 'null'
          description: Percentage of this domain's appearances that landed at positions
            1-3.
          title: Top3 Share
        top3_share_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: Change in `top3_share` between the two halves of the window,
            in percentage points. Null when the first half holds no appearance by
            this domain.
          title: Top3 Share Delta
        total_rank:
          anyOf:
          - type: integer
          - type: 'null'
          description: Position by `count` over the whole window, over the same population
            as `rank`.
          title: Total Rank
        trend:
          description: The domain's share at each check in the window, a percentage,
            oldest first.
          items:
            type: number
          title: Trend
          type: array
        trend_counts:
          description: The domain's count at each check, index-aligned with `trend`.
          items:
            anyOf:
            - type: integer
            - type: number
          title: Trend Counts
          type: array
      required:
      - name
      title: SerpCompetitorListItem
      type: object
    SerpCompetitorsResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: SERP competitors on this page.
          items:
            $ref: '#/components/schemas/SerpCompetitorListItem'
          title: Data
          type: array
        hero:
          anyOf:
          - $ref: '#/components/schemas/SerpHeroMetrics'
          - type: 'null'
          description: User-specific hero metrics for the same project window. Always
            present, may have null fields.
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        summary:
          $ref: '#/components/schemas/CompetitionSummaryBlock'
          description: Where the project stands in this ranking, over every row rather
            than this page.
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      - summary
      title: SerpCompetitorsResponse
      type: object
    SerpHeroMetrics:
      additionalProperties: true
      description: User-specific Google-organic SERP hero metrics for a project window.
      properties:
        avg_top_position:
          anyOf:
          - type: number
          - type: 'null'
          description: Mean of the user's best organic position across AIO checks
            where they appeared. Lower is better.
          title: Avg Top Position
        avg_top_position_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: Change in avg_top_position vs the previous half-window. Negative
            is an improvement.
          title: Avg Top Position Delta
        checks_total:
          anyOf:
          - type: integer
          - type: 'null'
          description: Number of AIO checks with non-empty SERP data in the window.
          title: Checks Total
        checks_with_user_position:
          anyOf:
          - type: integer
          - type: 'null'
          description: Number of those checks where the user appeared in the SERP.
          title: Checks With User Position
        featured_snippet_rate:
          anyOf:
          - type: number
          - type: 'null'
          description: Fraction (0-1) of AIO checks where the featured-snippet URL
            belongs to a user domain.
          title: Featured Snippet Rate
        featured_snippet_rate_delta:
          anyOf:
          - type: number
          - type: 'null'
          description: Change in featured_snippet_rate vs the previous half-window.
          title: Featured Snippet Rate Delta
      title: SerpHeroMetrics
      type: object
    SuccessScoreBreakdown:
      additionalProperties: true
      description: 'The metric breakdown behind one success score.


        `success_score` weights the three components rather than averaging them:

        `(citations + brand_mentions + sentiment * m) / (2 + m)`, where

        `m = brand_mentions / 100`, so sentiment counts for more as mentions grow.

        With no classified sentiment `sentiment` is null and the score is

        `(citations + brand_mentions) / 2`, which is why two buckets with the same

        `success_score` can rest on different denominators.


        Failed engine checks are excluded: every rate and every denominator here

        counts only the checks that answered. A bucket in which every check failed

        has no breakdown; the enclosing object reports how many failed.


        The three rate fields are percentages on a 0-100 scale. The `*_raw` and

        `*_denominator` counts are the integers those rates were computed from, so

        a consumer can re-derive or re-aggregate them rather than averaging averages.


        On a `combined` entry the counts are sums over the engines that answered,

        while the rates are the mean of those engines'' rates, so dividing a

        combined count by a combined denominator does not give the combined rate.

        To pool engines yourself, sum each engine''s counts rather than re-pooling

        `combined`.


        Every field is optional: this service and the upstream that computes these

        numbers are deployed separately, so a required field would turn an upstream

        that stops sending one into a 500 on a published endpoint.'
      properties:
        brand_appeared_count_raw:
          anyOf:
          - type: integer
          - type: 'null'
          description: Answers that either cited a tracked URL or named a tracked
            brand.
          title: Brand Appeared Count Raw
        brand_mentions:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of answered checks in the bucket that named a tracked
            brand (0-100). Failed checks are excluded.
          title: Brand Mentions
        citations:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of answered checks in the bucket that cited a tracked
            URL (0-100). Failed checks are excluded.
          title: Citations
        citations_count_raw:
          anyOf:
          - type: integer
          - type: 'null'
          description: Answers that cited a tracked URL.
          title: Citations Count Raw
        citations_denominator:
          anyOf:
          - type: integer
          - type: 'null'
          description: Answered checks analysed for citations - the citation-rate
            denominator. Failed checks are excluded.
          title: Citations Denominator
        denominator:
          anyOf:
          - type: integer
          - type: 'null'
          description: Answered checks in this bucket - the mention-rate denominator.
            Failed checks are excluded.
          title: Denominator
        mentions_count_raw:
          anyOf:
          - type: integer
          - type: 'null'
          description: Answers that named a tracked brand.
          title: Mentions Count Raw
        sentiment:
          anyOf:
          - type: number
          - type: 'null'
          description: Sentiment score across the classified answers (0-100). Null
            when no answer in the bucket carried a sentiment classification, in which
            case it is not a component of `success_score`.
          title: Sentiment
        sentiment_mixed:
          anyOf:
          - type: integer
          - type: 'null'
          description: Answers classified mixed.
          title: Sentiment Mixed
        sentiment_negative:
          anyOf:
          - type: integer
          - type: 'null'
          description: Answers classified negative.
          title: Sentiment Negative
        sentiment_positive:
          anyOf:
          - type: integer
          - type: 'null'
          description: Answers classified positive.
          title: Sentiment Positive
        success_score:
          anyOf:
          - type: number
          - type: 'null'
          description: 'Weighted composite of the components above (0-100): `(citations
            + brand_mentions + sentiment * m) / (2 + m)` with `m = brand_mentions
            / 100`, or `(citations + brand_mentions) / 2` when `sentiment` is null.'
          title: Success Score
      title: SuccessScoreBreakdown
      type: object
    TagCreateResponse:
      additionalProperties: true
      properties:
        color:
          description: Tag color (hex).
          title: Color
          type: string
        id:
          anyOf:
          - type: integer
          - type: 'null'
          description: Newly assigned tag identifier. May be null when the upstream
            service did not surface the new id (rare; the row is still created).
          title: Id
        name:
          description: Tag name.
          title: Name
          type: string
        prompt_count:
          default: 0
          description: Initial prompt count - always 0 on create.
          title: Prompt Count
          type: integer
      required:
      - name
      - color
      title: TagCreateResponse
      type: object
    TagListItem:
      additionalProperties: true
      properties:
        color:
          anyOf:
          - type: string
          - type: 'null'
          description: Hex color used in the UI for this tag (#RRGGBB). Always set.
          title: Color
        id:
          description: Tag identifier.
          title: Id
          type: integer
        name:
          description: Display name of the tag.
          title: Name
          type: string
        prompt_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Number of prompts associated with this tag.
          title: Prompt Count
      required:
      - id
      - name
      title: TagListItem
      type: object
    TagListResponse:
      additionalProperties: true
      properties:
        data:
          description: Tags on this page.
          items:
            $ref: '#/components/schemas/TagListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: TagListResponse
      type: object
    TagSuccessScoreBlock:
      additionalProperties: true
      description: Success score broken down by project tag, per engine.
      properties:
        bing_ai_overview:
          description: Per-tag scores for Bing AI Overview.
          items:
            $ref: '#/components/schemas/TagSuccessScoreEntry'
          title: Bing Ai Overview
          type: array
        chatgpt:
          description: Per-tag scores for ChatGPT.
          items:
            $ref: '#/components/schemas/TagSuccessScoreEntry'
          title: Chatgpt
          type: array
        combined:
          description: Per-tag scores across all enabled engines.
          items:
            $ref: '#/components/schemas/TagSuccessScoreEntry'
          title: Combined
          type: array
        copilot:
          description: Per-tag scores for Microsoft Copilot.
          items:
            $ref: '#/components/schemas/TagSuccessScoreEntry'
          title: Copilot
          type: array
        gemini:
          description: Per-tag scores for Gemini.
          items:
            $ref: '#/components/schemas/TagSuccessScoreEntry'
          title: Gemini
          type: array
        google_ai_mode:
          description: Per-tag scores for Google AI Mode.
          items:
            $ref: '#/components/schemas/TagSuccessScoreEntry'
          title: Google Ai Mode
          type: array
        google_aio:
          description: Per-tag scores for Google AI Overviews.
          items:
            $ref: '#/components/schemas/TagSuccessScoreEntry'
          title: Google Aio
          type: array
        perplexity:
          description: Per-tag scores for Perplexity.
          items:
            $ref: '#/components/schemas/TagSuccessScoreEntry'
          title: Perplexity
          type: array
      title: TagSuccessScoreBlock
      type: object
    TagSuccessScoreEntry:
      additionalProperties: true
      description: One project tag with the success score of the prompts carrying
        it.
      properties:
        color:
          anyOf:
          - type: string
          - type: 'null'
          description: Hex colour configured for the tag.
          title: Color
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed checks on this engine for the tag's prompts in the window,
            left out of `success_score`. On `combined`, the sum over every engine.
            Null when not reported.
          title: Failed Checks
        id_tag:
          anyOf:
          - type: integer
          - type: 'null'
          description: Identifier of the tag.
          title: Id Tag
        name:
          anyOf:
          - type: string
          - type: 'null'
          description: Tag name.
          title: Name
        success_score:
          anyOf:
          - $ref: '#/components/schemas/SuccessScoreBreakdown'
          - type: 'null'
          description: Score breakdown across the prompts carrying this tag, over
            the checks that answered. Null when every check on this engine for the
            tag's prompts failed.
      title: TagSuccessScoreEntry
      type: object
    TagUpdateResponse:
      additionalProperties: true
      properties:
        color:
          description: Current tag color (hex) after the update.
          title: Color
          type: string
        id:
          description: Tag identifier.
          title: Id
          type: integer
        name:
          description: Current tag name after the update.
          title: Name
          type: string
        prompt_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Number of prompts carrying the tag, read back after the update.
          title: Prompt Count
      required:
      - id
      - name
      - color
      title: TagUpdateResponse
      type: object
    TopBrandEntry:
      additionalProperties: true
      description: 'One mentioned brand in a ranking, with the count it was ranked
        by.


        Brand names here are canonical: spelling variants of one brand are merged

        before the counts are taken. See `GET /v1/projects/{project_id}/brand-aliases`

        for the merges your workspace configured.'
      properties:
        brand:
          anyOf:
          - type: string
          - type: 'null'
          description: Canonical brand name.
          title: Brand
        color:
          anyOf:
          - type: string
          - type: 'null'
          description: Hex colour assigned to this row by rank in the ziptie.ai charts.
            Presentation only - it carries no measurement and is not stable across
            windows.
          title: Color
        count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Mentions of this brand in the window.
          title: Count
      title: TopBrandEntry
      type: object
    TopBrandsBlock:
      additionalProperties: true
      description: Most-mentioned brands in the window, ranked per engine.
      properties:
        bing_ai_overview:
          description: Ranked for Bing AI Overview.
          items:
            $ref: '#/components/schemas/TopBrandEntry'
          title: Bing Ai Overview
          type: array
        chatgpt:
          description: Ranked for ChatGPT.
          items:
            $ref: '#/components/schemas/TopBrandEntry'
          title: Chatgpt
          type: array
        combined:
          description: Ranked across all enabled engines.
          items:
            $ref: '#/components/schemas/TopBrandEntry'
          title: Combined
          type: array
        copilot:
          description: Ranked for Microsoft Copilot.
          items:
            $ref: '#/components/schemas/TopBrandEntry'
          title: Copilot
          type: array
        gemini:
          description: Ranked for Gemini.
          items:
            $ref: '#/components/schemas/TopBrandEntry'
          title: Gemini
          type: array
        google_ai_mode:
          description: Ranked for Google AI Mode.
          items:
            $ref: '#/components/schemas/TopBrandEntry'
          title: Google Ai Mode
          type: array
        google_aio:
          description: Ranked for Google AI Overviews.
          items:
            $ref: '#/components/schemas/TopBrandEntry'
          title: Google Aio
          type: array
        perplexity:
          description: Ranked for Perplexity.
          items:
            $ref: '#/components/schemas/TopBrandEntry'
          title: Perplexity
          type: array
      title: TopBrandsBlock
      type: object
    TopCitationsBlock:
      additionalProperties: true
      description: Most-cited domains in the window, ranked per engine.
      properties:
        bing_ai_overview:
          description: Ranked for Bing AI Overview.
          items:
            $ref: '#/components/schemas/TopDomainEntry'
          title: Bing Ai Overview
          type: array
        chatgpt:
          description: Ranked for ChatGPT.
          items:
            $ref: '#/components/schemas/TopDomainEntry'
          title: Chatgpt
          type: array
        combined:
          description: Ranked across all enabled engines.
          items:
            $ref: '#/components/schemas/TopDomainEntry'
          title: Combined
          type: array
        copilot:
          description: Ranked for Microsoft Copilot.
          items:
            $ref: '#/components/schemas/TopDomainEntry'
          title: Copilot
          type: array
        gemini:
          description: Ranked for Gemini.
          items:
            $ref: '#/components/schemas/TopDomainEntry'
          title: Gemini
          type: array
        google_ai_mode:
          description: Ranked for Google AI Mode.
          items:
            $ref: '#/components/schemas/TopDomainEntry'
          title: Google Ai Mode
          type: array
        google_aio:
          description: Ranked for Google AI Overviews.
          items:
            $ref: '#/components/schemas/TopDomainEntry'
          title: Google Aio
          type: array
        perplexity:
          description: Ranked for Perplexity.
          items:
            $ref: '#/components/schemas/TopDomainEntry'
          title: Perplexity
          type: array
      title: TopCitationsBlock
      type: object
    TopDomainEntry:
      additionalProperties: true
      description: One cited domain in a ranking, with the count it was ranked by.
      properties:
        color:
          anyOf:
          - type: string
          - type: 'null'
          description: Hex colour assigned to this row by rank in the ziptie.ai charts.
            Presentation only - it carries no measurement and is not stable across
            windows.
          title: Color
        count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Citations of this domain in the window.
          title: Count
        domain:
          anyOf:
          - type: string
          - type: 'null'
          description: 'The cited domain. A `google.com` row is not necessarily a
            link to Google: Google Shopping product panels carry Google''s own host,
            so they are counted here under it rather than under the merchant they
            advertise. Read `/dashboard/ugc-impact/{platform}` or a check''s `sources`
            for the `source_kind` of the individual rows behind a domain.'
          title: Domain
      title: TopDomainEntry
      type: object
    TopicClusterListItem:
      additionalProperties: true
      properties:
        description:
          anyOf:
          - type: string
          - type: 'null'
          description: LLM-generated cluster description.
          title: Description
        first_seen_check_no:
          anyOf:
          - type: integer
          - type: 'null'
          description: Earliest check_no a cluster member was seen on.
          title: First Seen Check No
        id_cluster:
          description: Cluster identifier.
          title: Id Cluster
          type: integer
        last_seen_check_no:
          anyOf:
          - type: integer
          - type: 'null'
          description: Latest check_no a cluster member was seen on.
          title: Last Seen Check No
        name:
          description: Human-readable cluster name.
          title: Name
          type: string
        occurrence_count:
          description: Sum of topic occurrence counts in the cluster.
          title: Occurrence Count
          type: integer
        size:
          description: Number of topics in the cluster.
          title: Size
          type: integer
      required:
      - id_cluster
      - name
      - size
      - occurrence_count
      title: TopicClusterListItem
      type: object
    TopicClusterListResponse:
      additionalProperties: true
      properties:
        data:
          description: Clusters on this page.
          items:
            $ref: '#/components/schemas/TopicClusterListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: TopicClusterListResponse
      type: object
    TopicListItem:
      additionalProperties: true
      properties:
        first_seen_check_no:
          anyOf:
          - type: integer
          - type: 'null'
          title: First Seen Check No
        id_topic:
          description: Topic identifier.
          title: Id Topic
          type: integer
        last_seen_check_no:
          anyOf:
          - type: integer
          - type: 'null'
          title: Last Seen Check No
        occurrence_count:
          description: Number of source citations this topic has accumulated.
          title: Occurrence Count
          type: integer
        phrase:
          description: The canonical topic phrase.
          title: Phrase
          type: string
      required:
      - id_topic
      - phrase
      - occurrence_count
      title: TopicListItem
      type: object
    TopicListResponse:
      additionalProperties: true
      properties:
        data:
          description: Topics on this page.
          items:
            $ref: '#/components/schemas/TopicListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: TopicListResponse
      type: object
    TrackedCompetitorItem:
      additionalProperties: true
      description: 'One brand or domain the project tracks as a competitor: an entity
        carrying

        at least one competitor tag. This is project configuration rather than a

        measurement.'
      properties:
        domain:
          anyOf:
          - type: string
          - type: 'null'
          description: The tracked domain for a `domain` tracked competitor. Always
            null for a `brand` tracked competitor, which tracks a name rather than
            a site.
          title: Domain
        id:
          description: Unique identifier of the tracked competitor. The same id appears
            as a member id under `competitor-tags/{tag_id}/members`.
          title: Id
          type: string
        name:
          description: Display name of the tracked competitor. For a `domain` tracked
            competitor this is the domain itself; for a `brand` tracked competitor
            it is the brand name.
          title: Name
          type: string
        tags:
          description: Competitor tags this brand or domain carries, in name order.
          items:
            $ref: '#/components/schemas/CompetitorTagRef'
          title: Tags
          type: array
        type:
          description: '''domain'' for a tracked website, ''brand'' for a tracked
            brand name.'
          title: Type
          type: string
      required:
      - id
      - type
      - name
      title: TrackedCompetitorItem
      type: object
    TrackedCompetitorListResponse:
      additionalProperties: true
      properties:
        data:
          description: 'Tracked competitors on this page: every brand and domain carrying
            at least one competitor tag.'
          items:
            $ref: '#/components/schemas/TrackedCompetitorItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: TrackedCompetitorListResponse
      type: object
    UgcImpactRow:
      additionalProperties: true
      properties:
        citations:
          description: Citations from this platform in the window.
          title: Citations
          type: integer
        per_engine:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          description: Per-engine citation count for this platform.
          title: Per Engine
        platform:
          description: UGC platform name (e.g. reddit, quora).
          title: Platform
          type: string
        share:
          description: Share of total UGC citations, as a percentage (0-100, 1 decimal
            place).
          title: Share
          type: number
      required:
      - platform
      - citations
      - share
      title: UgcImpactRow
      type: object
    UgcPlatformSentimentOverview:
      additionalProperties: true
      description: 'Aggregate sentiment across the platform''s brand-mentioned sources.


        All four fields are on a 0-100 scale, and all four are null when no

        brand-mentioned source on the platform carries a classified sentiment -

        there is no sentiment to report, and reporting zero would read as

        unanimously negative.'
      properties:
        negative_percentage:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of classified sources that are negative.
          title: Negative Percentage
        neutral_percentage:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of classified sources that are neutral.
          title: Neutral Percentage
        overall_score:
          anyOf:
          - type: number
          - type: 'null'
          description: 50 is neutral, 100 all-positive, 0 all-negative.
          title: Overall Score
        positive_percentage:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of classified sources that are positive.
          title: Positive Percentage
      title: UgcPlatformSentimentOverview
      type: object
    UgcPlatformSource:
      additionalProperties: true
      description: 'One cited URL on a UGC platform, merged across every check that
        cited it.


        A row is one URL, not one citation: every (check, engine) record for the

        same URL collapses into this entry, so `engines` is the distinct set of

        engines that cited it rather than a per-row engine. URLs that differ only

        by a trailing Chromium `#:~:text=` scroll-to-text fragment merge into the

        bare form, and `text_fragment_variant_count` reports how many distinct

        excerpt fragments were seen.'
      properties:
        covered_subtopics:
          anyOf:
          - items: {}
            type: array
          - type: 'null'
          description: Subtopics the source covers, unioned across merged rows.
          title: Covered Subtopics
        detected_attributes:
          anyOf:
          - items: {}
            type: array
          - type: 'null'
          description: Product or brand attributes the analysis found on the source.
          title: Detected Attributes
        engines:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Public names of every engine that cited this URL, sorted.
          title: Engines
        general_sentiment:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Sentiment toward the brand on this source: `positive`, `neutral`
            or `negative`. Null whenever `is_brand_mentioned` is not true - sentiment
            is only classified where there is a mention to be sentimental about.'
          title: General Sentiment
        is_brand_mentioned:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether one of the project's tracked brands is named on the
            source itself. Null means no analysis recorded a mention.
          title: Is Brand Mentioned
        prompts:
          anyOf:
          - items:
              $ref: '#/components/schemas/UgcPlatformSourcePrompt'
            type: array
          - type: 'null'
          description: The monitored prompts whose checks cited this URL, honouring
            the same `engines` and `tag_ids` filters as the request.
          title: Prompts
        publication_date:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 publication date of the source, where one was found.
          title: Publication Date
        source_domain:
          anyOf:
          - type: string
          - type: 'null'
          description: Bare hostname of the URL - the platform this row belongs to.
          title: Source Domain
        source_kind:
          default: web
          description: 'Which citation surface this row came from. `web` is an ordinary
            cited page. `shopping` is a Google Shopping product panel, which is not
            a link to a merchant and does not redirect to one, so its domain is always
            Google''s - read the title for the product and the brand, never the domain.
            Panels are included in every count on this API; nothing here excludes
            them by default. Treat the value set as open: a kind you do not recognise
            is a surface added after your client was written, not an error.'
          title: Source Kind
          type: string
        source_title:
          anyOf:
          - type: string
          - type: 'null'
          description: Title of the page, post or video where one was captured. Null
            on rows analysed before titles were recorded.
          title: Source Title
        source_url:
          anyOf:
          - type: string
          - type: 'null'
          description: The cited URL.
          title: Source Url
        summary:
          anyOf:
          - type: string
          - type: 'null'
          description: Generated summary of the dominant concerns and themes on this
            source. Null for rows analysed before summaries were recorded.
          title: Summary
        text_fragment_variant_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Distinct scroll-to-text excerpt fragments observed for this
            URL.
          title: Text Fragment Variant Count
      title: UgcPlatformSource
      type: object
    UgcPlatformSourceListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        brand_awareness_score:
          anyOf:
          - type: number
          - type: 'null'
          description: Share of the cited sources that name one of the project's tracked
            brands (0-100). Computed over the whole filtered set rather than the current
            page, so it does not move with `limit`/`offset` - but it does move with
            `engines`, `tag_ids` and `q`, because those narrow the set it is taken
            over. Zero when `total` is zero.
          title: Brand Awareness Score
        data:
          description: Cited sources on this page.
          items:
            $ref: '#/components/schemas/UgcPlatformSource'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        sentiment_overview:
          anyOf:
          - $ref: '#/components/schemas/UgcPlatformSentimentOverview'
          - type: 'null'
          description: Aggregate sentiment across the brand-mentioned sources.
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      title: UgcPlatformSourceListResponse
      type: object
    UgcPlatformSourcePrompt:
      additionalProperties: true
      description: One monitored prompt whose checks cited this source.
      properties:
        engines:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Public names of the engines that cited the source in that check.
          title: Engines
        last_check_no:
          anyOf:
          - type: integer
          - type: 'null'
          description: Newest check number of this prompt that cited the source.
          title: Last Check No
        last_cited_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of that check's latest analysis row.
          title: Last Cited At
        prompt_id:
          anyOf:
          - type: integer
          - type: 'null'
          description: Identifier of the monitored prompt.
          title: Prompt Id
        text:
          anyOf:
          - type: string
          - type: 'null'
          description: The prompt's tracked query text.
          title: Text
      title: UgcPlatformSourcePrompt
      type: object
    UgcSignalItem:
      additionalProperties: true
      description: One group of attributes or voices that share a meaning and the
        sources carrying it.
      properties:
        brand_mentioned_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: How many of those sources name one of the project's tracked
            brands.
          title: Brand Mentioned Count
        label:
          anyOf:
          - type: string
          - type: 'null'
          description: 'The group''s display text: one member wording as written,
            chosen as the group label. For a voice it is the whole line, quotes included,
            with the list marker and emphasis marks removed. Attributes and voices
            that mean the same thing are grouped together, so a group can hold several
            wordings; see `wordings` on each source.'
          title: Label
        platform_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Distinct platforms (source domains) those sources sit on.
          title: Platform Count
        source_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Distinct sources the item appears on. A URL counts once however
            many checks or engines cited it.
          title: Source Count
        sources:
          anyOf:
          - items:
              $ref: '#/components/schemas/UgcSignalSource'
            type: array
          - type: 'null'
          description: The sources the item was found on, newest check first, then
            by URL. At most 100; see `sources_truncated`.
          title: Sources
        sources_truncated:
          anyOf:
          - type: boolean
          - type: 'null'
          description: True when more than 100 sources carry the item and `sources`
            lists the 100 most recent.
          title: Sources Truncated
      title: UgcSignalItem
      type: object
    UgcSignalListResponse:
      additionalProperties: true
      properties:
        analyzed_source_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Distinct sources in scope that carry at least one item of this
            kind. It is the denominator for `source_count` and does not move with
            `q`, `limit` or `offset`.
          title: Analyzed Source Count
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: Groups on this page, most widespread first (`source_count`
            descending, then `label`).
          items:
            $ref: '#/components/schemas/UgcSignalItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      title: UgcSignalListResponse
      type: object
    UgcSignalSource:
      additionalProperties: true
      description: One source an attribute or user voice was found on.
      properties:
        engines:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Public names of every engine that cited this URL, sorted.
          title: Engines
        general_sentiment:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Sentiment toward the brand on this source: `positive`, `neutral`
            or `negative`. Null whenever `is_brand_mentioned` is not true.'
          title: General Sentiment
        is_brand_mentioned:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Whether one of the project's tracked brands is named on the
            source itself. Null means no analysis recorded a mention.
          title: Is Brand Mentioned
        quotes:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Verbatim fragments quoted inside a user voice line. Equals
            the quotes of the first entry of `wordings`. Always empty for attributes.
          title: Quotes
        source_domain:
          anyOf:
          - type: string
          - type: 'null'
          description: Hostname of the URL without a leading `www.`.
          title: Source Domain
        source_title:
          anyOf:
          - type: string
          - type: 'null'
          description: Title of the page, post or video where one was captured.
          title: Source Title
        source_url:
          anyOf:
          - type: string
          - type: 'null'
          description: The cited URL.
          title: Source Url
        text:
          anyOf:
          - type: string
          - type: 'null'
          description: The attribute or voice line as written on this source, cleaned
            of its list marker only. Equals the first entry of `wordings`. Wording
            differs between sources that share a group.
          title: Text
        wordings:
          anyOf:
          - items:
              $ref: '#/components/schemas/UgcSignalWording'
            type: array
          - type: 'null'
          description: Every distinct wording this source carries within the group,
            in the order the source lists them. A source holds more than one when
            it words the same idea in different ways. `text` and `quotes` repeat the
            first entry.
          title: Wordings
      title: UgcSignalSource
      type: object
    UgcSignalWording:
      additionalProperties: true
      description: One distinct way a source words the idea its group stands for.
      properties:
        quotes:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Verbatim fragments quoted inside a user voice line. Always
            empty for attributes.
          title: Quotes
        text:
          anyOf:
          - type: string
          - type: 'null'
          description: The attribute or voice line as written on the source, cleaned
            of its list marker only.
          title: Text
      title: UgcSignalWording
      type: object
    UgcSourceListItem:
      additionalProperties: true
      properties:
        created_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the most recent analysis of this
            source. Carries the same instant as `last_seen_at`, which is the upstream
            name for it, and is the field this endpoint's default sort orders by.
          title: Created At
        engine:
          anyOf:
          - type: string
          - type: 'null'
          description: Public name of the AI engine whose answer cited this source.
            Rows are grouped per engine, so the same URL appears once per engine that
            cited it.
          title: Engine
        id_source_analysis:
          anyOf:
          - type: integer
          - type: 'null'
          description: Source-analysis row identifier; None for aggregated rows.
          title: Id Source Analysis
        last_seen_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the most recent citation of this
            source.
          title: Last Seen At
        source_domain:
          anyOf:
          - type: string
          - type: 'null'
          description: Bare hostname extracted from source_url.
          title: Source Domain
        source_kind:
          default: web
          description: 'Which citation surface this row came from. `web` is an ordinary
            cited page. `shopping` is a Google Shopping product panel, which is not
            a link to a merchant and does not redirect to one, so its domain is always
            Google''s - read the title for the product and the brand, never the domain.
            Panels are included in every count on this API; nothing here excludes
            them by default. Treat the value set as open: a kind you do not recognise
            is a surface added after your client was written, not an error.'
          title: Source Kind
          type: string
        source_title:
          anyOf:
          - type: string
          - type: 'null'
          description: Page title of the source, when one was captured. Null on rows
            analysed before titles were recorded.
          title: Source Title
        source_url:
          description: The source URL.
          title: Source Url
          type: string
        summary:
          anyOf:
          - type: string
          - type: 'null'
          description: Structured markdown summary; nullable for legacy rows.
          title: Summary
        use_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Number of times this source was cited by this engine under
            this topic.
          title: Use Count
      required:
      - source_url
      title: UgcSourceListItem
      type: object
    UgcSourceListResponse:
      additionalProperties: true
      properties:
        data:
          description: Sources on this page.
          items:
            $ref: '#/components/schemas/UgcSourceListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      title: UgcSourceListResponse
      type: object
    UncertaintyChangeVerdict:
      description: Whether the change in a KPI between the first and latest scored
        day is likely real.
      properties:
        caveats:
          description: Extra cautions on the verdict. Currently `prompt_mix_differs`
            (the two days covered different prompts) and `retries_pending` (a day
            in the comparison is still inside the retry window, so the verdict can
            change; see `settles_at`). New values may be added; treat an unknown value
            as no extra information.
          items:
            type: string
          title: Caveats
          type: array
        prompts:
          description: The number of distinct prompts across the two days the verdict
            compares.
          title: Prompts
          type: integer
        reason:
          anyOf:
          - type: string
          - type: 'null'
          description: Why the verdict is what it is when a limit applied, or null.
            Currently one of `engines_differ`, `failure_heavy`, `too_many_prompts`
            or `sparse_engine`. New values may be added; treat an unknown value as
            no extra information.
          title: Reason
        verdict:
          description: Whether the change is likely a real difference. Currently one
            of `likely_real` (likely a real difference), `could_be_noise` (the change
            could be noise), `insufficient_sample` (too little data to say) or `not_comparable`
            (the two days are not comparable). It is an indicative output of the method,
            not a validated test. New values may be added; treat an unknown verdict
            as `could_be_noise`.
          title: Verdict
          type: string
      required:
      - verdict
      - prompts
      title: UncertaintyChangeVerdict
      type: object
    UncertaintyInterval:
      description: A likely range for one KPI value, and the sample behind it.
      properties:
        high:
          anyOf:
          - type: number
          - type: 'null'
          description: High end of the likely range, on the same scale as the KPI,
            rounded up to one decimal place, so the range contains the KPI value shown.
            Null when `reason` says why there is no range.
          title: High
        low:
          anyOf:
          - type: number
          - type: 'null'
          description: Low end of the likely range, on the same scale as the KPI (a
            percent for a rate, 0-100 for the sentiment score), rounded down to one
            decimal place. Null when `reason` says why there is no range. The range
            is an indicative output of the method, not a validated test.
          title: Low
        prompts:
          description: The number of distinct prompts in the sample. A small number
            widens the range.
          title: Prompts
          type: integer
        reason:
          anyOf:
          - type: string
          - type: 'null'
          description: Why there is no range (`low` and `high` are null), or null
            when there is one. Currently one of `no_data`, `low_sample`, `few_prompts`,
            `no_variation`, `not_applicable`, `too_many_prompts` or `sparse_engine`.
            New values may be added; treat an unknown value as no extra information.
          title: Reason
        sample:
          description: The size of the sample behind the range, counted in `sample_unit`.
            Failed checks are excluded.
          title: Sample
          type: integer
        sample_unit:
          description: What `sample` counts. Currently one of `answers`, `answers_with_links`,
            `answers_with_sentiment` or `checks`. New values may be added; treat an
            unknown value as no extra information.
          title: Sample Unit
          type: string
      required:
      - sample
      - sample_unit
      - prompts
      title: UncertaintyInterval
      type: object
    UncertaintyKpiChanges:
      description: Verdicts on the change of the covered headline KPIs, latest day
        against first day.
      properties:
        citations_rate:
          anyOf:
          - $ref: '#/components/schemas/UncertaintyChangeVerdict'
          - type: 'null'
          description: Verdict on the citations-rate change. Null when only one day
            was scored.
        mentions_rate:
          anyOf:
          - $ref: '#/components/schemas/UncertaintyChangeVerdict'
          - type: 'null'
          description: Verdict on the mentions-rate change. Null when only one day
            was scored.
        sentiment_score:
          anyOf:
          - $ref: '#/components/schemas/UncertaintyChangeVerdict'
          - type: 'null'
          description: Verdict on the sentiment-score change. Null when only one day
            was scored.
        success_score:
          description: 'Always null: the AI Success Score carries no verdict.'
          title: Success Score
          type: 'null'
      title: UncertaintyKpiChanges
      type: object
    UncertaintyKpiIntervals:
      description: Likely ranges for the covered headline KPIs on the latest scored
        day.
      properties:
        citations_rate:
          $ref: '#/components/schemas/UncertaintyInterval'
          description: Likely range for the citations rate on `latest_date`.
        mentions_rate:
          $ref: '#/components/schemas/UncertaintyInterval'
          description: Likely range for the mentions rate on `latest_date`.
        sentiment_score:
          $ref: '#/components/schemas/UncertaintyInterval'
          description: Likely range for the sentiment score on `latest_date`.
        success_score:
          description: 'Always null: the AI Success Score carries no range.'
          title: Success Score
          type: 'null'
      required:
      - citations_rate
      - mentions_rate
      - sentiment_score
      title: UncertaintyKpiIntervals
      type: object
    UrlCreateResponse:
      additionalProperties: true
      properties:
        id:
          anyOf:
          - type: integer
          - type: 'null'
          description: Newly assigned URL identifier. May be null when the upstream
            service did not surface the new id (rare; the row is still created).
          title: Id
        prompt_count:
          default: 0
          description: Initial prompt count - always 0 on create.
          title: Prompt Count
          type: integer
        url:
          description: The added URL.
          title: Url
          type: string
      required:
      - url
      title: UrlCreateResponse
      type: object
    UrlDetailResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        check_dates:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: ISO-8601 dates corresponding to entries in ``trend``.
          title: Check Dates
        failed_checks:
          anyOf:
          - type: integer
          - type: 'null'
          description: Failed engine checks left out of the citation rates in ``trend``.
            Null when not reported.
          title: Failed Checks
        id:
          description: URL identifier.
          title: Id
          type: integer
        linked_prompts:
          anyOf:
          - items:
              $ref: '#/components/schemas/UrlLinkedPrompt'
            type: array
          - type: 'null'
          description: Prompts that link to this URL.
          title: Linked Prompts
        trend:
          anyOf:
          - items:
              type: number
            type: array
          - type: 'null'
          description: 'Per-check citation rate (0-1, four decimal places) aligned
            to ``check_dates``. Each entry is the URL''s citation rate at the corresponding
            check, over the answered engine checks: a failed check, and a `google_aio`
            or `bing_ai_overview` run that showed no overview, is not an answer. A
            date with no answered check has no rate and is left out of both arrays.'
          title: Trend
        url:
          description: The tracked URL.
          title: Url
          type: string
      required:
      - applied
      - id
      - url
      title: UrlDetailResponse
      type: object
    UrlLinkedPrompt:
      additionalProperties: true
      properties:
        cited:
          anyOf:
          - type: boolean
          - type: 'null'
          description: True when any engine cited the URL for this prompt on its latest
            check date.
          title: Cited
        engines:
          anyOf:
          - additionalProperties:
              anyOf:
              - type: boolean
              - type: 'null'
            type: object
          - type: 'null'
          description: 'Per engine on the prompt''s latest check date, keyed by public
            engine name: true when it cited the URL, false when it did not, null when
            its check failed. An engine with no check on that date is absent, and
            so is a `google_aio` or `bing_ai_overview` run on that date that showed
            no overview.'
          title: Engines
        failed_engines:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: Public engine names whose latest check for this prompt failed.
            A failed engine neither cited nor missed the URL, so it is left out of
            the citation rate.
          title: Failed Engines
        id:
          description: Linked prompt identifier.
          title: Id
          type: integer
        last_cited_at:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 timestamp of the most recent check, across the prompt's
            full history and every engine, at which the prompt cited the URL. Null
            when it never has.
          title: Last Cited At
        text:
          description: Linked prompt text.
          title: Text
          type: string
      required:
      - id
      - text
      title: UrlLinkedPrompt
      type: object
    UrlListItem:
      additionalProperties: true
      properties:
        citation_rate:
          anyOf:
          - type: number
          - type: 'null'
          description: 'Fraction of answered checks that cited this URL (0-1): citations
            over the answered checks of the prompts linked to it, at each prompt''s
            latest check, on the engines in `applied.engines`. A failed check, and
            a `google_aio` or `bing_ai_overview` run that showed no overview, is not
            an answered check. Null when there is none: no linked prompt, none checked
            yet, or every check failed or showed no overview. Null sorts lowest.'
          title: Citation Rate
        id:
          description: URL identifier.
          title: Id
          type: integer
        last_cited:
          anyOf:
          - type: string
          - type: 'null'
          description: ISO-8601 UTC timestamp of the most recent citation by a linked
            prompt, on any engine, whatever `engines` names.
          title: Last Cited
        prompt_count:
          description: Number of prompts that link to this URL.
          title: Prompt Count
          type: integer
        url:
          description: The tracked URL.
          title: Url
          type: string
      required:
      - id
      - url
      - prompt_count
      title: UrlListItem
      type: object
    UrlListResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: The window and engines this response covers, after defaults
            and clamping. Read it rather than assuming the request's values were applied
            as sent.
        data:
          description: URLs on this page.
          items:
            $ref: '#/components/schemas/UrlListItem'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - applied
      - data
      title: UrlListResponse
      type: object
    UrlUpdateResponse:
      additionalProperties: true
      properties:
        id:
          description: URL identifier.
          title: Id
          type: integer
        prompt_count:
          anyOf:
          - type: integer
          - type: 'null'
          description: Number of prompts that link to this URL, read back after the
            update.
          title: Prompt Count
        url:
          description: Current URL after the update.
          title: Url
          type: string
      required:
      - id
      - url
      title: UrlUpdateResponse
      type: object
    ValidationError:
      properties:
        ctx:
          title: Context
          type: object
        input:
          title: Input
        loc:
          items:
            anyOf:
            - type: string
            - type: integer
          title: Location
          type: array
        msg:
          title: Message
          type: string
        type:
          title: Error Type
          type: string
      required:
      - loc
      - msg
      - type
      title: ValidationError
      type: object
    WebsiteBeforeAfter:
      additionalProperties: true
      description: 'Citations and Search Console around the page''s anchor publish
        date.


        The anchor is the latest confirmed publish date among the content pieces on

        the page; earlier confirmed dates are listed. The comparison reads only after

        at least 3 checks and 14 days have passed since the anchor.'
      properties:
        anchor_date:
          description: The anchor publish date, `YYYY-MM-DD`.
          format: date
          title: Anchor Date
          type: string
        checks_after:
          description: Check cycles after the anchor date so far.
          title: Checks After
          type: integer
        citations:
          additionalProperties: true
          description: Targeted-prompt counts, `before` (null when the page had no
            earlier checks) and `after`, each with `prompts`, `cited`, `mentioned`
            and `cited_or_mentioned` (`after` also `cited_any`), plus `failed_only_prompts`.
            Counts before and after a publish date are a comparison of what was observed,
            not a measure of cause and effect.
          title: Citations
          type: object
        days_after:
          description: Days after the anchor date so far.
          title: Days After
          type: integer
        earlier_dates:
          description: Earlier confirmed publish dates for the page, oldest first.
          items:
            format: date
            type: string
          title: Earlier Dates
          type: array
        range:
          anyOf:
          - additionalProperties: true
            type: object
          - type: 'null'
          description: The uncertainty range for each side's Citations Rate where
            enough answers exist, otherwise `not_enough_data` with the answers and
            prompts counted.
          title: Range
        search_console:
          additionalProperties: true
          description: 'The Search Console comparison: `state` (`compared`, `not_enough_history`,
            `history_loading`, `not_connected`, `no_data` or `too_early`), `window_days`,
            `before` and `after` impressions and clicks (absolute counts, no percentages),
            `history_from`, and the `property` read. A before figure is never computed
            from part of its window.'
          title: Search Console
          type: object
        state:
          description: '`too_early` until enough checks and days have passed after
            the anchor.'
          enum:
          - compared
          - too_early
          title: State
          type: string
      required:
      - anchor_date
      - earlier_dates
      - state
      - checks_after
      - days_after
      - citations
      - search_console
      title: WebsiteBeforeAfter
      type: object
    WebsiteCitingPrompt:
      additionalProperties: true
      description: A prompt whose checks cited the page, whether or not it is linked
        to it.
      properties:
        engines_cited:
          description: Public engine names that cited the page.
          items:
            type: string
          title: Engines Cited
          type: array
        id:
          description: The prompt id.
          title: Id
          type: integer
        last_cited_day:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: The last day a check cited the page for this prompt.
          title: Last Cited Day
        text:
          description: The prompt text.
          title: Text
          type: string
      required:
      - id
      - text
      - engines_cited
      title: WebsiteCitingPrompt
      type: object
    WebsiteListApplied:
      additionalProperties: false
      description: The `applied` echo of the website pages list.
      properties:
        date_from:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: First day read, `YYYY-MM-DD` (UTC). Null only on `checks/answers`
            in `latest` or `check_no` mode, which read no window.
          title: Date From
        date_to:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: Last day read, `YYYY-MM-DD` (UTC), after a `date_to` later
            than today is clamped to today. Null exactly when `date_from` is.
          title: Date To
        engines:
          anyOf:
          - items:
              type: string
            type: array
          - type: 'null'
          description: 'Public engine names the numbers cover: the requested `engines`
            the workspace''s plan includes, or the plan''s engines when `engines`
            is omitted. An engine you asked for that the plan lacks is not here, which
            is how a silently narrowed filter shows. Null where the read has no per-engine
            data (Search Console, the organic SERP rankings).'
          title: Engines
        scope:
          description: '`all`, `added` (added URLs only) or `content`.'
          title: Scope
          type: string
        search_console_properties:
          description: The number of Search Console properties connected to the project,
            each reporting its own domains. 0 when none is connected.
          title: Search Console Properties
          type: integer
        search_console_states:
          additionalProperties:
            type: integer
          description: How many pages matched by `search_console.state` (`ok`, `not_connected`,
            `no_data`), over all pages the filters match, not just this page of results.
          title: Search Console States
          type: object
        seen_in:
          description: '`any`, `citations` or `search_console`.'
          title: Seen In
          type: string
        window_days:
          description: 'The rolling window the `*_window` figures cover, in days ending
            at the latest check. `date_from` and `date_to` are null: the window follows
            the checks, not the calendar.'
          title: Window Days
          type: integer
      required:
      - date_from
      - date_to
      - engines
      - window_days
      - scope
      - seen_in
      - search_console_properties
      - search_console_states
      title: WebsiteListApplied
      type: object
    WebsitePageDetail:
      additionalProperties: true
      description: 'One website page in full: per-check series, prompts, and the publish-date
        comparison.'
      properties:
        added_twice:
          description: Whether two added URLs resolve to this page (different spellings).
          title: Added Twice
          type: boolean
        applied:
          $ref: '#/components/schemas/AppliedFilters'
          description: 'The engines and window this response covers. `date_from` and
            `date_to` are null when you sent neither: the series then covers the 90
            days to the latest check.'
        before_after:
          anyOf:
          - $ref: '#/components/schemas/WebsiteBeforeAfter'
          - type: 'null'
          description: The comparison around the anchor publish date. Null when no
            content piece on the page has a confirmed publish date. Counts before
            and after a publish date are a comparison of what was observed, not a
            measure of cause and effect.
        citing_prompts:
          description: Prompts whose checks cited the page, linked or not.
          items:
            $ref: '#/components/schemas/WebsiteCitingPrompt'
          title: Citing Prompts
          type: array
        domain:
          anyOf:
          - type: string
          - type: 'null'
          description: The project domain the page belongs to.
          title: Domain
        first_cited_day:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: First day a check cited it.
          title: First Cited Day
        folder_key:
          description: The page's folder.
          title: Folder Key
          type: string
        host:
          description: The page's host.
          title: Host
          type: string
        is_added:
          description: Whether the page is among the project's added URLs.
          title: Is Added
          type: boolean
        last_cited_day:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: Last day a check cited it.
          title: Last Cited Day
        page_id:
          description: The page id, as listed on the pages endpoint.
          title: Page Id
          type: string
        page_key:
          description: The normalised address, host plus path.
          title: Page Key
          type: string
        prompts:
          description: Prompts linked to the page as their target.
          items:
            $ref: '#/components/schemas/WebsiteTargetedPrompt'
          title: Prompts
          type: array
        search_console:
          $ref: '#/components/schemas/WebsiteSearchConsole'
          description: The state of the page's Search Console figures and the property
            behind them.
        series:
          description: One point per check cycle in the window, oldest first.
          items:
            $ref: '#/components/schemas/WebsiteSeriesPoint'
          title: Series
          type: array
        url:
          description: A clickable address for the page, tracking parameters removed.
          title: Url
          type: string
        url_id:
          anyOf:
          - type: integer
          - type: 'null'
          description: The added URL id, or null when the page is not an added URL.
          title: Url Id
        variant_count:
          description: Spellings of the address merged into this page.
          title: Variant Count
          type: integer
        why:
          description: Up to three reasons the figures look as they do, by code and
            parameters.
          items:
            $ref: '#/components/schemas/WebsiteWhyCode'
          title: Why
          type: array
      required:
      - page_id
      - page_key
      - url
      - folder_key
      - host
      - variant_count
      - is_added
      - added_twice
      - search_console
      - series
      - prompts
      - citing_prompts
      - why
      - applied
      title: WebsitePageDetail
      type: object
    WebsitePageRow:
      additionalProperties: true
      description: 'One page of the project''s website: its citations and its Search
        Console figures.'
      properties:
        citations_window:
          description: Citations of the page across all checks in the `window_days`
            window.
          title: Citations Window
          type: integer
        clicks_window:
          anyOf:
          - type: integer
          - type: 'null'
          description: Search Console clicks for the page over the window, all countries.
            Null unless `search_console.state` is `ok`. Not split by engine.
          title: Clicks Window
        domain:
          anyOf:
          - type: string
          - type: 'null'
          description: The project domain the page belongs to, or null for a page
            on another site.
          title: Domain
        failed_checks_window:
          description: Checks in the window that failed, so cited-or-not is unknown
            for them.
          title: Failed Checks Window
          type: integer
        first_cited_day:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: The first day any check cited the page, or null if none did.
          title: First Cited Day
        folder_key:
          description: 'The page''s folder: its host and first path segment, or the
            host root.'
          title: Folder Key
          type: string
        has_content_piece:
          description: Whether a ziptie.ai content piece is connected to the page.
          title: Has Content Piece
          type: boolean
        host:
          description: The page's host.
          title: Host
          type: string
        impressions_window:
          anyOf:
          - type: integer
          - type: 'null'
          description: 'Search Console impressions for the page over the window, all
            countries. Null unless `search_console.state` is `ok`. Not split by engine:
            it does not change with `engines`.'
          title: Impressions Window
        is_added:
          description: Whether the page is among the URLs added to the project.
          title: Is Added
          type: boolean
        page_id:
          description: 'Stable identifier of the page: the lower-case SHA-256 hex
            of its normalised address. Pass it to `GET /v1/projects/{project_id}/website/pages/{page_id}`.
            Spellings of one address that differ only by scheme, `www.`, query string,
            fragment or a trailing slash are one page; path case is kept, so `/Blog`
            and `/blog` are two.'
          title: Page Id
          type: string
        page_key:
          description: The normalised address the page is identified by, host plus
            path.
          title: Page Key
          type: string
        prompts_citing_latest:
          description: Prompts whose latest check cited this page, over the requested
            engines.
          title: Prompts Citing Latest
          type: integer
        search_console:
          $ref: '#/components/schemas/WebsiteSearchConsole'
          description: The state of the page's Search Console figures and the property
            behind them.
        targeted_cited_latest:
          description: Targeted prompts whose latest check cited the page.
          title: Targeted Cited Latest
          type: integer
        targeted_cited_or_mentioned_latest:
          description: Targeted prompts whose latest check cited the page or mentioned
            the brand, counted once each.
          title: Targeted Cited Or Mentioned Latest
          type: integer
        targeted_mentioned_latest:
          description: Targeted prompts whose latest check mentioned the brand.
          title: Targeted Mentioned Latest
          type: integer
        targeted_prompts:
          description: Prompts the page is linked to as their target page.
          title: Targeted Prompts
          type: integer
        url:
          description: A clickable address for the page, tracking parameters removed.
          title: Url
          type: string
        url_id:
          anyOf:
          - type: integer
          - type: 'null'
          description: The id of the added URL on `GET /v1/projects/{project_id}/urls/{url_id}`.
            Null when the page is not an added URL.
          title: Url Id
        variant_count:
          description: How many spellings of the address were seen; they are merged
            into this row.
          title: Variant Count
          type: integer
      required:
      - page_id
      - page_key
      - url
      - folder_key
      - host
      - is_added
      - has_content_piece
      - prompts_citing_latest
      - targeted_prompts
      - targeted_cited_latest
      - targeted_mentioned_latest
      - targeted_cited_or_mentioned_latest
      - citations_window
      - search_console
      - variant_count
      - failed_checks_window
      title: WebsitePageRow
      type: object
    WebsitePagesResponse:
      additionalProperties: true
      properties:
        applied:
          $ref: '#/components/schemas/WebsiteListApplied'
          description: The engines, window and filters this response covers.
        data:
          description: Website pages on this page.
          items:
            $ref: '#/components/schemas/WebsitePageRow'
          title: Data
          type: array
        limit:
          description: Page size requested.
          title: Limit
          type: integer
        offset:
          description: Page offset requested.
          title: Offset
          type: integer
        total:
          description: Total number of records matching the filter.
          title: Total
          type: integer
      required:
      - total
      - limit
      - offset
      - data
      - applied
      title: WebsitePagesResponse
      type: object
    WebsitePromptAfter:
      additionalProperties: true
      description: One targeted prompt on the after side of the anchor date, read
        from its last cycle there.
      properties:
        cited:
          description: Whether the prompt's latest check cycle after the anchor date
            cited the page.
          title: Cited
          type: boolean
        cited_any:
          description: Whether any of the prompt's check cycles after the anchor date
            cited the page.
          title: Cited Any
          type: boolean
        cycles:
          description: How many of the prompt's check cycles fall on the after side.
          title: Cycles
          type: integer
        mentioned:
          description: Whether the prompt's latest check cycle after the anchor date
            mentioned the brand.
          title: Mentioned
          type: boolean
      required:
      - cited
      - mentioned
      - cited_any
      - cycles
      title: WebsitePromptAfter
      type: object
    WebsitePromptBefore:
      additionalProperties: true
      description: One targeted prompt on the before side of the anchor date, read
        from its last cycle there.
      properties:
        cited:
          description: Whether the prompt's last check cycle before the anchor date
            cited the page.
          title: Cited
          type: boolean
        cycles:
          description: How many of the prompt's check cycles fall on the before side
            (up to 90 days).
          title: Cycles
          type: integer
        mentioned:
          description: Whether the prompt's last check cycle before the anchor date
            mentioned the brand.
          title: Mentioned
          type: boolean
      required:
      - cited
      - mentioned
      - cycles
      title: WebsitePromptBefore
      type: object
    WebsiteSearchConsole:
      additionalProperties: true
      description: Where one page's Search Console figures come from.
      properties:
        property:
          anyOf:
          - type: string
          - type: 'null'
          description: The Search Console property that reports this page's domain,
            exactly as Google names it (`sc-domain:example.com` or a URL prefix).
            One property owns each domain, so a page never mixes figures from two.
            Null when the state is `not_connected`.
          title: Property
        state:
          description: '`ok`: the page has Search Console figures. `no_data`: the
            page''s domain is connected but Search Console holds nothing for this
            page in the window. `not_connected`: no connected property reports the
            page''s domain. `impressions_window` and `clicks_window` are null unless
            the state is `ok`, never zero.'
          enum:
          - ok
          - not_connected
          - no_data
          title: State
          type: string
      required:
      - state
      - property
      title: WebsiteSearchConsole
      type: object
    WebsiteSeriesPoint:
      additionalProperties: true
      description: One check cycle of a page.
      properties:
        check_no:
          description: The check number.
          title: Check No
          type: integer
        cycle_day:
          description: The UTC day the check cycle started.
          format: date
          title: Cycle Day
          type: string
        failed_prompts:
          description: Prompts whose check failed in this cycle.
          title: Failed Prompts
          type: integer
        prompts_citing:
          description: Prompts that cited the page in this cycle.
          title: Prompts Citing
          type: integer
        targeted_cited:
          description: Targeted prompts that cited the page in this cycle.
          title: Targeted Cited
          type: integer
        targeted_mentioned:
          description: Targeted prompts that mentioned the brand in this cycle.
          title: Targeted Mentioned
          type: integer
      required:
      - check_no
      - cycle_day
      - targeted_cited
      - targeted_mentioned
      - prompts_citing
      - failed_prompts
      title: WebsiteSeriesPoint
      type: object
    WebsiteTargetedPrompt:
      additionalProperties: true
      description: A prompt the page is linked to as its target page.
      properties:
        added_after_publishing:
          description: Whether the prompt was linked after the anchor publish date.
            Such a prompt has no `before` figure and counts only on the after side.
          title: Added After Publishing
          type: boolean
        after:
          anyOf:
          - $ref: '#/components/schemas/WebsitePromptAfter'
          - type: 'null'
          description: The prompt's cycles after the anchor date, or null when it
            has none there. Per-prompt flags, not counts.
        before:
          anyOf:
          - $ref: '#/components/schemas/WebsitePromptBefore'
          - type: 'null'
          description: The prompt's cycles before the anchor date (up to 90 days),
            or null when it has none there. Per-prompt flags, not counts.
        brand_mentioned_latest:
          description: Whether the latest check mentioned the brand for this prompt.
          title: Brand Mentioned Latest
          type: boolean
        engines_cited:
          description: Public engine names whose latest check cited the page for this
            prompt.
          items:
            type: string
          title: Engines Cited
          type: array
        id:
          description: The prompt id.
          title: Id
          type: integer
        last_cited_day:
          anyOf:
          - format: date
            type: string
          - type: 'null'
          description: The last day a check cited the page for this prompt.
          title: Last Cited Day
        linked_at:
          anyOf:
          - type: string
          - type: 'null'
          description: When the prompt was linked to the page, ISO 8601.
          title: Linked At
        only_failed_latest:
          description: Whether every engine's latest check for this prompt failed.
          title: Only Failed Latest
          type: boolean
        text:
          description: The prompt text.
          title: Text
          type: string
      required:
      - id
      - text
      - added_after_publishing
      - engines_cited
      - brand_mentioned_latest
      - only_failed_latest
      title: WebsiteTargetedPrompt
      type: object
    WebsiteWhyCode:
      additionalProperties: true
      description: One reason the comparison looks the way it does, as a code with
        its parameters.
      properties:
        code:
          description: A stable code such as `too_early`, `not_cited`, `partly_cited`,
            `impressions_not_cited`, `not_in_search_console`, `no_before`, `not_enough_history`
            or `unchanged`. Render your own sentence from the code and `params`; the
            codes are the contract, not any English wording.
          title: Code
          type: string
        params:
          additionalProperties: true
          description: The numbers and names the code refers to.
          title: Params
          type: object
      required:
      - code
      - params
      title: WebsiteWhyCode
      type: object
  securitySchemes:
    ApiToken:
      bearerFormat: zt_live_...
      description: 'A `zt_live_...` API token, sent as `Authorization: Bearer <token>`.

        There are two kinds, and every operation accepts both. A project

        token reaches one project. A workspace token, minted by a workspace

        Owner or Admin, reaches every project of its workspace at the moment

        of each request, including projects added after it was minted, and

        always expires (30, 90 or 365 days). Each token carries one scope,

        `read`, `write` or `admin`, whatever its kind. A workspace token stops

        working, with 401 `TOKEN_REVOKED`, once the Owner or Admin who created

        it holds neither role in the workspace, and the writes it makes are

        attributed to that person.

        '
      scheme: bearer
      type: http
info:
  contact:
    email: contact@ziptie.ai
    name: ziptie.ai Support
    url: https://ziptie.ai/about/#contact-section
  description: 'The ziptie.ai Public API exposes project data (project configuration,

    prompts, tags, URLs, dashboard metrics, competitors, GSC, website pages,

    UGC impact)

    plus the calling token''s own rate limits, for programmatic access. Every

    endpoint requires a `zt_live_...` API token, and every endpoint except

    `GET /v1/projects`, which lists the projects the token reaches, is

    project-scoped. See https://docs.ziptie.ai/ for guides and setup.


    The surface carries operations on the data ziptie.ai gathers, and nothing

    else. Workspace, account and subscription management are out of scope by

    design, so there is no endpoint for them and none is planned.


    **Standard errors** - All endpoints may return these codes regardless

    of the specific operation: `401 MISSING_AUTH_HEADER` / `INVALID_AUTH_FORMAT`

    / `TOKEN_INVALID` / `TOKEN_REVOKED` / `TOKEN_EXPIRED`, `403 PROJECT_MISMATCH`

    / `API_ADDON_INACTIVE` / `ENTITLEMENT_DENIED` / `INSUFFICIENT_SCOPE`,

    `402 PAYMENT_REQUIRED`, `422 VALIDATION_ERROR`, `429 RATE_LIMITED`,

    `500 INTERNAL_ERROR`, `503 SERVICE_UNAVAILABLE` / `MAINTENANCE_MODE`.

    Per-endpoint error tables below list only endpoint-specific codes. See

    [error codes](/content/error-codes.html) for the full registry.


    **Project access** - `403 PROJECT_MISMATCH` on a project-scoped endpoint

    means the project is not reachable by this token (another project for a

    project token, another workspace for a workspace token). A project that does not

    exist answers the same way, never 404. Call `GET /v1/projects` to list the

    project ids the token reaches. `503 SERVICE_UNAVAILABLE` on a

    project-scoped endpoint can also mean the token''s access to the project

    could not be checked at that moment: retry it, because it never means the

    access was lost. Over MCP the first is JSON-RPC `-32002` and the second

    `-32000`, each with `data.code` set.


    **Subscription state** - API access follows the same subscription rule

    as the ziptie.ai app. `402 PAYMENT_REQUIRED` means the first payment is

    not yet confirmed, or the workspace''s subscription has ended and the

    workspace is closed. `403 API_ADDON_INACTIVE` means the API Access add-on

    is off, the workspace is on a trial, or it has no subscription at all. A

    workspace in dunning, cancelling at period end or waiting on a payment

    confirmation keeps full access while it is inside its paid period. Once

    the paid period has ended the workspace is read-only: every read keeps

    working, every write (POST, PATCH, DELETE and the matching MCP tools)

    returns `403 SUBSCRIPTION_READ_ONLY`, and `GET /v1/projects/{project_id}`

    reports `write_access: false`. `503 SERVICE_UNAVAILABLE` means the

    subscription check itself could not be made; retry.


    **Engine filters** - Every endpoint that filters on `engines` answers

    `400 INVALID_ENGINE_NAME` for a name that is not a public engine key,

    and `400 ENGINE_NOT_ENABLED` when every engine it names is a real one

    the workspace''s subscription does not include. A filter that names at

    least one included engine is not refused: the engines the subscription

    lacks are dropped and the response covers the rest. Over MCP both codes

    arrive as JSON-RPC `-32602`. Drop the engines the workspace lacks, or

    omit `engines`.


    The statuses above are the `/v1/` statuses. **No error on `/mcp` uses a

    status code at all**: every rejection, including the authentication and

    entitlement ones decided in front of the router, is an HTTP `200` carrying

    a JSON-RPC `error` member. Branch on `error.code` there, never on the

    transport status.


    **Maintenance windows** - During a declared maintenance window every

    endpoint returns `503 MAINTENANCE_MODE` with an `X-Maintenance-Mode: 1`

    header and a `Retry-After` value derived from the window''s ETA. The body

    carries a `maintenance` object with the operator''s `message` and

    `ends_at`. Wait out `Retry-After` and retry; the window covers the whole

    platform, so no endpoint or project is a way around it. Over MCP the same

    condition arrives as JSON-RPC `-32006` with `data.code` set to

    `MAINTENANCE_MODE` and a `retry_after_seconds` hint.


    **Rate limits** - Exceeding any bucket returns `429 RATE_LIMITED` with a

    `Retry-After` header and the `X-RateLimit-Limit` / `-Remaining` / `-Reset`

    trio. Over MCP the same condition arrives as JSON-RPC `-32003` at HTTP

    `200`, with `data.code` set to `RATE_LIMITED` and a `retry_after_seconds`

    hint; the `Retry-After` header rides along. Every MCP error is an HTTP

    `200` carrying a JSON-RPC `error` member, so branch on `error.code` rather

    than on the transport status. One MCP `tools/call` costs one unit in each

    bucket, the same as one `/v1/` request; `initialize` and `tools/list` cost

    nothing. The per-token minute bucket of a workspace token is its own,

    lower limit; the workspace minute and day buckets are shared by every

    token of the workspace, of both kinds, so one integration''s traffic counts

    against the others''.


    **Authentication and entitlement over MCP** - A missing, malformed,

    revoked, expired or invalid token arrives as JSON-RPC `-32001` at HTTP

    `200`, with `data.code` naming which of them it was. An entitlement

    refusal - `PAYMENT_REQUIRED`, `API_ADDON_INACTIVE` or

    `ENTITLEMENT_DENIED` - arrives as `-32004`. Both were previously bare

    `401` / `402` / `403` responses carrying the REST envelope, which an MCP

    client could only report as a transport failure. The `/v1/` surface still

    answers those conditions with their real status codes. A read-only

    subscription (`SUBSCRIPTION_READ_ONLY`) and a plan cap

    (`PROMPT_LIMIT_REACHED`, `INSUFFICIENT_CREDITS`, `TAG_LIMIT_REACHED`,

    `URL_LIMIT_REACHED`) also arrive as `-32004`, and `PROJECT_NOT_FOUND` as

    `-32005`. A code without its own JSON-RPC number follows the HTTP status it

    carries on `/v1/`: `400` and `422` are `-32602`, `401` is `-32001`, `402`

    is `-32004`, `403` is `-32002`, `404` is `-32005`, `409` is `-32009`,

    `429` is `-32003`, and any other status is `-32000`. `data.code` always

    carries the REST code, so branch on it for the exact condition.

    '
  license:
    name: Proprietary
  title: ziptie.ai Public API
  version: 1.22.2
openapi: 3.1.0
paths:
  /mcp:
    post:
      description: 'Single JSON-RPC 2.0 endpoint for the Model Context Protocol. This
        is

        not a REST endpoint - the request and response bodies are JSON-RPC

        envelopes (`{jsonrpc, id, method, params}` in, `{jsonrpc, id, result}`

        or `{jsonrpc, id, error}` out). Use this to plug ziptie.ai into MCP-aware

        clients such as Claude Code, Claude Desktop, Cursor, and VS Code.


        Supported methods are `initialize` (handshake, returns server info and

        protocol version), `tools/list` (returns the catalogue of callable

        tools with their input schemas), and `tools/call` (invokes a tool by

        name with arguments). The OpenAPI operation cannot describe the

        polymorphic body; per-tool input schemas come from the `tools/list`

        response. `initialize` and `tools/list` are exempt from rate-limit

        counting.


        HTTP errors are translated to JSON-RPC error codes. See the

        [MCP JSON-RPC error mappings](/content/error-codes.html#mcp-json-rpc-error-mappings)

        table for the full mapping. The `data` field of every MCP error contains

        the original `{"code": "...", "error": "..."}` envelope.


        See [MCP overview](/content/mcp/overview.html) for protocol details and the

        per-client setup guides under `/content/mcp/<client>.md` (claude-code,

        claude-desktop, cursor, vscode, generic).

        '
      operationId: mcp_entry_mcp_post
      requestBody:
        content:
          application/json:
            example:
              id: 1
              jsonrpc: '2.0'
              method: tools/list
              params: {}
      responses:
        '200':
          content:
            application/json:
              example:
                id: 1
                jsonrpc: '2.0'
                result:
                  tools:
                  - description: Returns the prompts tracked in a project, paginated
                      and sorted.
                    inputSchema:
                      properties:
                        limit:
                          default: 50
                          description: Maximum number of items to return per page.
                            Defaults to 50.
                          type: integer
                        offset:
                          default: 0
                          description: Zero-based offset into the result set for pagination.
                          type: integer
                        project_id:
                          description: Unique identifier of the project.
                          type: string
                      required:
                      - project_id
                      type: object
                    name: list_prompts
                  - description: 'Returns the project''s prompts as the app''s Prompts
                      table shows them: each prompt''s AI Score, its change over the
                      window, its score trend, and every engine''s answer at its latest
                      check.'
                    inputSchema:
                      properties:
                        project_id:
                          description: Unique identifier of the project.
                          type: string
                        signal_filters:
                          description: Comma-separated rules `[+|-]<engine>:<signal>`,
                            all of which a prompt must satisfy.
                          type: string
                      required:
                      - project_id
                      type: object
                    name: list_prompt_performance
                  - description: Returns the summary card the app shows above its
                      Prompts table.
                    inputSchema:
                      properties:
                        project_id:
                          description: Unique identifier of the project.
                          type: string
                      required:
                      - project_id
                      type: object
                    name: get_prompts_summary
                  - description: Returns a single prompt with its 30-day rollup stats
                      and per-engine breakdown.
                    inputSchema:
                      properties:
                        project_id:
                          description: Unique identifier of the project.
                          type: string
                        prompt_id:
                          description: Unique identifier of the prompt.
                          type: integer
                      required:
                      - project_id
                      - prompt_id
                      type: object
                    name: get_prompt
                  - description: Creates a new prompt under the project and returns
                      it with the assigned id.
                    inputSchema:
                      properties:
                        project_id:
                          description: Unique identifier of the project.
                          type: string
                      required:
                      - project_id
                      type: object
                    name: create_prompt
              schema: {}
          description: JSON-RPC 2.0 response envelope. See MCP guides.
      summary: MCP JSON-RPC entry
      tags:
      - MCP
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/mcp\" \\\n  -H \"Authorization:\
          \ Bearer zt_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n\
          \  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\"\
          :{}}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/mcp\", {\n  method:\
          \ \"POST\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\",\n\
          \    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\
          \ jsonrpc: \"2.0\", id: 1, method: \"tools/list\", params: {} }),\n});\n\
          const data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.post(\n    \"https://api.ziptie.ai/mcp\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n    json={\"\
          jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\", \"params\": {}},\n\
          )\ndata = res.json()\n"
  /v1/projects:
    get:
      description: 'Lists the projects this token reaches, one page at a time. Call
        this

        first (the MCP tool is `list_projects`) to find the `project_id` every

        other endpoint takes, then read each project with

        `GET /v1/projects/{project_id}`.


        Each item carries the configuration and freshness fields of

        `GET /v1/projects/{project_id}` that are cheap to read for many

        projects at once - `id`, `name`, `country`, `language`,

        `check_frequency_days`, `enabled_engines`, `last_check_date`,

        `first_check_complete` and `created_at` - with the same names, types

        and meanings, so code that parses the detail read parses an item.

        Counts, domains, brands, the Search Console property and

        `write_access` stay on the detail read. `enabled_engines` is `null`

        when the workspace plan allows no engine, exactly as on the detail

        read, and like it is derived from the plan rather than the project:

        re-read it rather than caching it.


        The list follows the token''s access at the moment of the request, so

        re-list rather than caching the ids. The workspace identifier is

        deliberately not part of an item.


        Sort with `sort=name:asc` (the default), `name:desc`,

        `created_at:asc` or `created_at:desc`. See

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `VALIDATION_ERROR` | 422 | `limit` is outside 1-100, `offset` is negative,
        the `sort` field is not `name` or `created_at`, or `engines`, `tag_ids`, `url_ids`
        or `q` is sent |

        | `SERVICE_UNAVAILABLE` | 503 | The project list could not be read at that
        moment; retry |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_projects
      parameters:
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - check_frequency_days: 7
                  country: US
                  created_at: '2026-01-05T12:00:00Z'
                  enabled_engines:
                    bing_ai_overview: false
                    chatgpt: true
                    copilot: false
                    gemini: false
                    google_ai_mode: false
                    google_aio: true
                    perplexity: true
                  first_check_complete: true
                  id: abc123
                  language: en
                  last_check_date: '2026-08-20T09:30:00Z'
                  name: Acme Running Shoes
                limit: 50
                offset: 0
                total: 1
              schema:
                $ref: '#/components/schemas/ProjectListResponse'
          description: Paginated list of the projects this token reaches, each with
            the configuration and freshness fields of `GET /v1/projects/{project_id}`
            that are cheap to read for many projects.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List projects
      tags:
      - Projects
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects?limit=50\" \\\n  -H \"Authorization:\
          \ Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects?limit=50\"\
          , {\n  headers: { Authorization: \"Bearer zt_live_...\" }\n});\nconst {\
          \ data, total } = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects\"\
          ,\n    params={\"limit\": 50},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\nprojects = res.json()[\"data\"]\n"
      x-required-scope: read
  /v1/projects/{project_id}:
    get:
      description: 'Returns one project this token reaches. Call `GET /v1/projects`
        (the

        MCP tool `list_projects`) first to find the project ids, then this.

        Every other endpoint on this surface is scoped to a project but

        describes none of it, so read this before the data: it is what lets a

        client label its own dashboard, work out whether anything is being

        checked at all, and tell whether the numbers it is about to read are

        fresh.


        The payload has two halves. The configuration half - `domains`,

        `brands`, `country`, `language`, `device`, `check_frequency_days`,

        `enabled_engines`, `prompt_count` and `monitored_prompt_count` - is

        what the checks were told to measure, so it

        explains the shape of every result elsewhere, including why an engine

        has no rows anywhere on this surface. The freshness half -

        `last_check_date`, `current_cycle` and `first_check_complete` - is the

        "is there anything new?" primitive to poll before re-reading data.

        `last_check_date` is the project-level cycle timestamp, which is not

        the same as the per-prompt `last_check_date` on

        `/v1/projects/{project_id}/prompts`.


        `enabled_engines` is keyed by public engine name and says which

        engines the workspace plan allows to be checked. It is `null` when the

        plan allows none, which is what a lapsed subscription, or one whose

        period has not started yet, looks like from the outside. Read `null`

        as "the plan permits no monitoring", not as a missing field.


        It is a permission, not a schedule. Whether anything is actually

        checked also depends on `monitored_prompt_count`, the subset of

        `prompt_count` still switched on for monitoring: while that is `0` no

        check runs for the project whatever `enabled_engines` lists, and

        `last_check_date` and `current_cycle` do not move. Read the two

        together before concluding monitoring is healthy. They are separate

        fields on purpose, because they call for different responses - a

        `null` roster is a billing problem, a `0` monitored count is a

        customer who switched their own prompts off.


        `country` and `language` together are the third reason a permitted

        engine can hold no rows, and it is the only one that is silent per

        engine rather than per project. Not every engine can be checked in

        every territory, and Google AI Overviews, Google AI Mode and Bing AI

        Overview cannot be checked in every language, so an engine that

        `enabled_engines` marks `true` is skipped entirely for a project whose

        `country` or `language` it cannot serve: no check is scheduled,

        nothing is billed, and no error appears against the prompt, because

        the gap is a coverage fact rather than a fault. The other engines keep

        running and the freshness fields keep moving, so a project can look

        perfectly healthy while one engine on it has never produced a row.

        Read a permanently empty engine together with `country` and

        `language` before treating it as a defect.

        The coverage lists move as engines expand, so treat the pairing as

        current rather than fixed, and re-read it rather than caching it.


        The freshness half goes quiet in either case, so `last_check_date` and

        `current_cycle` cannot on their own tell you apart a project between

        cycles from one that has stopped. `domains` and `brands` are always

        arrays and are empty rather than `null` when nothing is connected. A

        `project_id` the token does not reach is refused with 403

        `PROJECT_MISMATCH` rather than 404.


        `enabled_engines` is derived from the workspace plan, not from a

        per-project engine selection, so it can change without any change to

        the project and without a new check being run. Do not cache it as

        stable project configuration: a plan change leaves `last_check_date`

        and `current_cycle` untouched, so a freshness poll reports nothing new

        while a cached engine set is already wrong. Re-read it whenever you

        act on it.


        `write_access` says whether the workspace subscription grants write

        access now. `false` means read-only: every read keeps working and every

        write (POST/PATCH/DELETE and the matching MCP tools) is refused with

        403 `SUBSCRIPTION_READ_ONLY` until the subscription is renewed. A

        workspace in dunning, cancelling at period end or waiting on a payment

        confirmation is `true` while it is still inside its paid period. It is

        independent of token scope: a `read`-only token on a writable

        workspace still sees `true`.


        The workspace identifier is deliberately not in this payload, and

        neither is any subscription management surface: this surface carries

        operations on the data ziptie.ai gathers.


        See [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `PROJECT_NOT_FOUND` | 404 | The project no longer exists upstream |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_project_detail
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      responses:
        '200':
          content:
            application/json:
              example:
                brands:
                - Acme
                check_frequency_days: 7
                country: US
                created_at: '2026-01-05T12:00:00Z'
                current_cycle: 12
                device: desktop
                domains:
                - acme.com
                - acme.co.uk
                enabled_engines:
                  bing_ai_overview: false
                  chatgpt: true
                  copilot: false
                  gemini: false
                  google_ai_mode: false
                  google_aio: true
                  perplexity: true
                first_check_complete: true
                gsc_property: sc-domain:acme.com
                id: abc123
                language: en
                last_check_date: '2026-08-20T09:30:00Z'
                monitored_prompt_count: 44
                name: Acme Running Shoes
                prompt_count: 48
                url_count: 12
                write_access: true
              schema:
                $ref: '#/components/schemas/ProjectDetailResponse'
          description: The project's configuration and check-freshness block, plus
            the plan-derived engine roster and whether the workspace may write now.
            The workspace identifier is deliberately not part of this payload.
        '404':
          content:
            application/json:
              example:
                code: PROJECT_NOT_FOUND
                error: project not found
          description: Project no longer exists upstream
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get project
      tags:
      - Projects
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123\" \\\n  -H \"Authorization:\
          \ Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123\"\
          , {\n  headers: { Authorization: \"Bearer zt_live_...\" }\n});\nconst project\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\nproject\
          \ = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/answer-accuracy/problems:
    get:
      description: 'Lists the problems of a project: facts that AI answers contradict.
        A

        problem opens when an answer states something else about a registered

        fact, and carries the engines, the claims in the engines'' own words,

        and the count of contradicting answers. ziptie.ai uses an AI model to

        compare each answer with your facts. It can be wrong: dismiss a

        problem that is not one, or edit the fact.


        `status` keeps `open` (default), `resolved`, `dismissed` or

        `fact_changed` problems; `total` counts the problems under the

        filters. A problem on a paused fact is listed under `open`, with

        `fact.status` set to `paused`: it is frozen (not checked). A problem

        with no recorded contradicting answer has a null

        `last_contradiction_at`; a claim without an identifier has a null

        `claim_id`. `engines` narrows the engines whose contradictions count; a

        problem whose contradictions are all on other engines is returned with

        `outside_filter: true`. `claims` holds up to three claims, newest

        first; the detail read has them all. `since_dismissal_count` is set

        only on a dismissed problem and counts contradicting answers checked

        since the dismissal.


        `sort` takes `last_contradiction_at` (default), `opened_at` or

        `answer_count`, all newest or largest first: use `:desc`. The figures

        are counts, never rates. The list is counted over a fixed window of

        checks, so `date_from`, `date_to`, `tag_ids`, `url_ids` and `q` are

        refused with 422 `VALIDATION_ERROR`. See

        [Pagination](/content/guides/pagination.html) and

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        Part of paid plans: a request whose plan cannot be confirmed as paid

        gets `402 PAID_PLAN_REQUIRED`. A lapsed workspace keeps reading.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |

        | `VALIDATION_ERROR` | 422 | A filter that does not apply was sent, `status`
        is not a listed value, or `sort` is not a listed field in `:desc` form |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_answer_accuracy_problems_v1_projects__project_id__answer_accuracy_problems_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Only problems with this status: `open` (default), `resolved`,
          `dismissed` or `fact_changed`.'
        in: query
        name: status
        required: false
        schema:
          default: open
          description: 'Only problems with this status: `open` (default), `resolved`,
            `dismissed` or `fact_changed`.'
          enum:
          - open
          - resolved
          - dismissed
          - fact_changed
          title: Status
          type: string
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - answer_count: 3
                  claims:
                  - answer_count: 3
                    claim_id: 0123456789abcdef
                    claimed_value: 59 USD
                    engines:
                    - chatgpt
                    - google_aio
                    last_seen: '2026-10-20T08:00:00+00:00'
                    quote: Pro costs 59 USD per month
                  dismiss_reason: null
                  dismissed_until: null
                  engines:
                  - chatgpt
                  - google_aio
                  fact:
                    category: price
                    id: b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40
                    statement: The Pro plan costs 49 USD a month.
                    status: active
                    version: 1
                  id: 11111111-2222-3333-4444-555555555555
                  last_contradiction_at: '2026-10-20T08:00:00+00:00'
                  opened_at: '2026-10-10T08:00:00+00:00'
                  outside_filter: false
                  resolution: null
                  resolution_cause: null
                  resolved_at: null
                  since_dismissal_count: null
                  status: open
                limit: 50
                offset: 0
                total: 1
              schema:
                $ref: '#/components/schemas/AccuracyProblemListResponse'
          description: Problems, offset paged.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              example:
                code: FEATURE_DISABLED
                error: Answer accuracy is not available yet.
          description: Answer accuracy is not switched on yet
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List answer accuracy problems
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems?status=open&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems?status=open&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems\"\
          ,\n    params={\"status\": \"open\", \"limit\": 50},\n    headers={\"Authorization\"\
          : \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/answer-accuracy/problems/{problem_id}:
    get:
      description: 'Returns one problem in full: the fact as it was judged (`fact`)
        and, when

        it was edited since, as it reads now (`fact_current`, null when equal);

        every claim in the engines'' own words; per-engine counts of answers

        that contradict, support or leave the fact unclear; the pages the

        contradicting answers cited; and the problem''s history. ziptie.ai uses

        an AI model to compare each answer with your facts. It can be wrong:

        dismiss a problem that is not one, or edit the fact.


        `sources` splits the cited pages into `own` (your domains) and `other`

        sites, each with `cited_in`, the count of contradicting answers that

        cited it, out of `answers_total`; `answers_without_sources` counts the

        contradicting answers that cited nothing. A citation shows where an

        answer drew from, never that the page caused the error. `engines`

        narrows the engines whose answers are counted. The figures are counts,

        never rates. List the contradicting answers with

        `GET /answer-accuracy/problems/{problem_id}/answers`.


        Part of paid plans: a request whose plan cannot be confirmed as paid

        gets `402 PAID_PLAN_REQUIRED`. A lapsed workspace keeps reading.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `NOT_FOUND` | 404 | The problem does not exist or belongs to a different
        project |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_answer_accuracy_problem_v1_projects__project_id__answer_accuracy_problems__problem_id__get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the answer-accuracy problem.
        in: path
        name: problem_id
        required: true
        schema:
          description: Unique identifier of the answer-accuracy problem.
          maxLength: 64
          minLength: 1
          title: Problem Id
          type: string
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      responses:
        '200':
          content:
            application/json:
              example:
                answer_count: 3
                claims:
                - answer_count: 3
                  claim_id: 0123456789abcdef
                  claimed_value: 59 USD
                  engines:
                  - chatgpt
                  - google_aio
                  last_seen: '2026-10-20T08:00:00+00:00'
                  quote: Pro costs 59 USD per month
                dismiss_reason: null
                dismissed_until: null
                engine_counts:
                - contradicts: 2
                  engine: chatgpt
                  supports: 1
                  unclear: 0
                - contradicts: 1
                  engine: google_aio
                  supports: 0
                  unclear: 1
                engines:
                - chatgpt
                - google_aio
                fact:
                  category: price
                  id: b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40
                  statement: The Pro plan costs 49 USD a month.
                  status: active
                  version: 1
                fact_current:
                  category: price
                  statement: The Pro plan costs 59 USD a month.
                  version: 2
                history:
                - at: '2026-10-10T08:00:00+00:00'
                  detail:
                    engines:
                    - chatgpt
                  kind: opened
                id: 11111111-2222-3333-4444-555555555555
                last_contradiction_at: '2026-10-20T08:00:00+00:00'
                opened_at: '2026-10-10T08:00:00+00:00'
                outside_filter: false
                resolution: null
                resolution_cause: null
                resolved_at: null
                since_dismissal_count: null
                sources:
                  answers_total: 3
                  answers_without_sources: 1
                  other:
                  - cited_in: 1
                    url: https://other.example.org/review
                  own:
                  - cited_in: 2
                    url: https://example.com/pricing
                status: open
              schema:
                $ref: '#/components/schemas/AccuracyProblemDetailResponse'
          description: The problem in full.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              examples:
                feature_disabled:
                  summary: Answer accuracy is not switched on yet
                  value:
                    code: FEATURE_DISABLED
                    error: Answer accuracy is not available yet.
                not_found:
                  summary: The problem is not in this project
                  value:
                    code: NOT_FOUND
                    error: Problem not found
          description: The problem is not in this project, or answer accuracy is not
            switched on yet
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get answer accuracy problem
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/answer-accuracy/problems/{problem_id}/answers:
    get:
      description: 'Lists the answers that contradict a problem''s fact in its current

        episode, offset paged. Each row names the prompt (`prompt_id`, the id

        the prompts endpoints use), the engine, the check (`check_no`, as in

        `/prompts/{prompt_id}/checks/{check_no}`), the UTC day and time of the

        check (`check_date`, `checked_at`), the engine''s own words (`quote`),

        the fact version the answer was judged against and the claim it

        belongs to. ziptie.ai uses an AI model to compare each answer with

        your facts. It can be wrong: dismiss a problem that is not one, or

        edit the fact.


        `engines` keeps answers from those engines and `claim_id` keeps the

        answers that made one claim, as `claim_id` on a claim of the problem.

        `total` counts the answers under the filters. The list is counted

        over a fixed window of checks, so `date_from`, `date_to`, `tag_ids`,

        `url_ids`, `q` and `sort` are refused with 422 `VALIDATION_ERROR`. See

        [Pagination](/content/guides/pagination.html).


        Part of paid plans: a request whose plan cannot be confirmed as paid

        gets `402 PAID_PLAN_REQUIRED`. A lapsed workspace keeps reading.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `NOT_FOUND` | 404 | The problem does not exist or belongs to a different
        project |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |

        | `VALIDATION_ERROR` | 422 | A filter that does not apply was sent, or `claim_id`
        is not 16 lowercase hex characters |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_answer_accuracy_problem_answers_v1_projects__project_id__answer_accuracy_problems__problem_id__answers_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the answer-accuracy problem.
        in: path
        name: problem_id
        required: true
        schema:
          description: Unique identifier of the answer-accuracy problem.
          maxLength: 64
          minLength: 1
          title: Problem Id
          type: string
      - description: Only answers that made this claim, as `claim_id` on a claim.
        in: query
        name: claim_id
        required: false
        schema:
          anyOf:
          - pattern: ^[0-9a-f]{16}$
            type: string
          - type: 'null'
          description: Only answers that made this claim, as `claim_id` on a claim.
          title: Claim Id
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - check_date: '2026-10-20'
                  check_no: 7
                  checked_at: '2026-10-20T08:15:00+00:00'
                  claim_id: 0123456789abcdef
                  claimed_value: 59 USD
                  engine: chatgpt
                  fact_version: 1
                  prompt_id: 11
                  prompt_text: best crm for small teams
                  quote: Pro costs 59 USD per month
                limit: 50
                offset: 0
                total: 3
              schema:
                $ref: '#/components/schemas/AccuracyAnswersResponse'
          description: The contradicting answers of the problem, offset paged.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              examples:
                feature_disabled:
                  summary: Answer accuracy is not switched on yet
                  value:
                    code: FEATURE_DISABLED
                    error: Answer accuracy is not available yet.
                not_found:
                  summary: The problem is not in this project
                  value:
                    code: NOT_FOUND
                    error: Problem not found
          description: The problem is not in this project, or answer accuracy is not
            switched on yet
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List answer accuracy problem answers
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/answers?limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/answers?limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/answers\"\
          ,\n    params={\"limit\": 50},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/answer-accuracy/problems/{problem_id}/dismiss:
    post:
      description: 'Dismisses an open problem: use it when the answers are right and
        the

        fact is the thing that is out of date, or when the judgement was

        wrong. A dismissed problem is held for 30 days, and `dismissed_until`

        names the end of the hold. It reopens after the hold only when a

        contradicting answer is checked after it; `since_dismissal_count`

        counts the contradicting answers checked since the dismissal.

        ziptie.ai uses an AI model to compare each answer with your facts. It

        can be wrong: dismiss a problem that is not one, or edit the fact.


        `reason` is optional: `not_wrong`, `harmless` or `other`. The dismissal

        lands in the app as the app''s own does, recorded under the actor

        `api`, and answers the problem as it reads afterwards. Needs the

        `write` token scope.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `NOT_FOUND` | 404 | The problem does not exist or belongs to a different
        project |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |

        | `PROBLEM_NOT_DISMISSABLE` | 409 | The problem is not open, so there is nothing
        to dismiss |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: dismiss_answer_accuracy_problem_v1_projects__project_id__answer_accuracy_problems__problem_id__dismiss_post
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the answer-accuracy problem.
        in: path
        name: problem_id
        required: true
        schema:
          description: Unique identifier of the answer-accuracy problem.
          maxLength: 64
          minLength: 1
          title: Problem Id
          type: string
      requestBody:
        content:
          application/json:
            example:
              reason: harmless
            schema:
              $ref: '#/components/schemas/DismissProblemBody'
      responses:
        '200':
          content:
            application/json:
              example:
                answer_count: 3
                claims:
                - answer_count: 3
                  claim_id: 0123456789abcdef
                  claimed_value: 59 USD
                  engines:
                  - chatgpt
                  - google_aio
                  last_seen: '2026-10-20T08:00:00+00:00'
                  quote: Pro costs 59 USD per month
                dismiss_reason: harmless
                dismissed_until: '2026-11-26T09:00:00+00:00'
                engines:
                - chatgpt
                - google_aio
                fact:
                  category: price
                  id: b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40
                  statement: The Pro plan costs 49 USD a month.
                  status: active
                  version: 1
                id: 11111111-2222-3333-4444-555555555555
                last_contradiction_at: '2026-10-20T08:00:00+00:00'
                opened_at: '2026-10-10T08:00:00+00:00'
                outside_filter: false
                resolution: null
                resolution_cause: null
                resolved_at: null
                since_dismissal_count: 0
                status: dismissed
              schema:
                $ref: '#/components/schemas/AccuracyProblemItem'
          description: The problem after the dismissal.
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              examples:
                feature_disabled:
                  summary: Answer accuracy is not switched on yet
                  value:
                    code: FEATURE_DISABLED
                    error: Answer accuracy is not available yet.
                not_found:
                  summary: The problem is not in this project
                  value:
                    code: NOT_FOUND
                    error: Problem not found
          description: The problem is not in this project, or answer accuracy is not
            switched on yet
        '409':
          content:
            application/json:
              example:
                code: PROBLEM_NOT_DISMISSABLE
                error: Only an open problem can be dismissed.
          description: Conflict - the problem is not open
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Dismiss answer accuracy problem
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/dismiss\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -d '{\"reason\":\"harmless\"}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/dismiss\"\
          ,\n  {\n    method: \"POST\",\n    headers: {\n      Authorization: \"Bearer\
          \ zt_live_...\",\n      \"Content-Type\": \"application/json\",\n    },\n\
          \    body: JSON.stringify({ reason: \"harmless\" }),\n  }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/dismiss\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n    json={\"\
          reason\": \"harmless\"},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/answer-accuracy/problems/{problem_id}/undismiss:
    post:
      description: 'Undoes a dismissal: the problem is open again and counts toward

        `open_count`. The call takes no body and answers the problem as it

        reads afterwards. The undismissal lands in the app as the app''s own

        does, recorded under the actor `api`. Needs the `write` token scope.

        ziptie.ai uses an AI model to compare each answer with your facts. It

        can be wrong: dismiss a problem that is not one, or edit the fact.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `NOT_FOUND` | 404 | The problem does not exist or belongs to a different
        project |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |

        | `PROBLEM_NOT_DISMISSED` | 409 | The problem is not dismissed, so there is
        nothing to undo |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: undismiss_answer_accuracy_problem_v1_projects__project_id__answer_accuracy_problems__problem_id__undismiss_post
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the answer-accuracy problem.
        in: path
        name: problem_id
        required: true
        schema:
          description: Unique identifier of the answer-accuracy problem.
          maxLength: 64
          minLength: 1
          title: Problem Id
          type: string
      responses:
        '200':
          content:
            application/json:
              example:
                answer_count: 3
                claims:
                - answer_count: 3
                  claim_id: 0123456789abcdef
                  claimed_value: 59 USD
                  engines:
                  - chatgpt
                  - google_aio
                  last_seen: '2026-10-20T08:00:00+00:00'
                  quote: Pro costs 59 USD per month
                dismiss_reason: null
                dismissed_until: null
                engines:
                - chatgpt
                - google_aio
                fact:
                  category: price
                  id: b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40
                  statement: The Pro plan costs 49 USD a month.
                  status: active
                  version: 1
                id: 11111111-2222-3333-4444-555555555555
                last_contradiction_at: '2026-10-20T08:00:00+00:00'
                opened_at: '2026-10-10T08:00:00+00:00'
                outside_filter: false
                resolution: null
                resolution_cause: null
                resolved_at: null
                since_dismissal_count: null
                status: open
              schema:
                $ref: '#/components/schemas/AccuracyProblemItem'
          description: The problem after the dismissal is undone.
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              examples:
                feature_disabled:
                  summary: Answer accuracy is not switched on yet
                  value:
                    code: FEATURE_DISABLED
                    error: Answer accuracy is not available yet.
                not_found:
                  summary: The problem is not in this project
                  value:
                    code: NOT_FOUND
                    error: Problem not found
          description: The problem is not in this project, or answer accuracy is not
            switched on yet
        '409':
          content:
            application/json:
              example:
                code: PROBLEM_NOT_DISMISSED
                error: Only a dismissed problem can be undone.
          description: Conflict - the problem is not dismissed
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Undismiss answer accuracy problem
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/undismiss\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/undismiss\"\
          ,\n  { method: \"POST\", headers: { Authorization: \"Bearer zt_live_...\"\
          \ } }\n);\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/problems/11111111-2222-3333-4444-555555555555/undismiss\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/answer-accuracy/summary:
    get:
      description: 'Returns where answer accuracy stands for a project, as counts
        over the

        last 30 days: open problems, answers checked and not checked (with the

        reasons), the facts registered, and when the latest check ran. ziptie.ai

        uses an AI model to compare each answer with your facts. It can be

        wrong: dismiss a problem that is not one, or edit the fact.


        `state` is a named state, never a score. `no_facts`: no facts are

        registered. `facts_paused`: facts exist and none is active, so nothing

        is checked. `waiting`: there are active facts and nothing has been

        checked in the window yet. `no_contradictions`: at least one answer was

        checked and none contradicts a fact. `problems`: at least one open

        problem. `no_contradictions` is only ever reported when

        `checked_count` is above zero; a project with nothing checked is

        `waiting`, never clean.


        `open_count` counts open problems whose fact is active.

        `paused_open_count` counts open problems whose fact is paused: they

        are frozen (not checked) and not in `open_count`.


        `not_checked_count` is answers that were not checked, split in

        `not_checked_by_reason`. They are never counted as correct. The

        counts are not rates: this surface returns counts and named states

        only.


        `engines` narrows the answer counts (`open_count`,

        `paused_open_count`, `checked_count`, `not_mentioned_count`,

        `not_checked_count` and `not_checked_by_reason`) to those engines;

        `facts_count`, `active_facts_count` and `last_checked_at` do not

        follow it. The counts are over a fixed window of checks, so

        `date_from`, `date_to`, `tag_ids`, `url_ids`, `q`, `sort`, `limit` and

        `offset` are refused with 422 `VALIDATION_ERROR`.


        Part of paid plans: a request whose plan cannot be confirmed as paid

        gets `402 PAID_PLAN_REQUIRED`. A lapsed workspace keeps reading.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_answer_accuracy_summary_v1_projects__project_id__answer_accuracy_summary_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      responses:
        '200':
          content:
            application/json:
              example:
                active_facts_count: 4
                checked_count: 40
                facts_count: 5
                last_checked_at: '2026-10-26T06:00:00+00:00'
                not_checked_by_reason:
                  answer_missing: 0
                  invalid_output: 1
                  judge_failed: 1
                  spend_cap: 1
                not_checked_count: 3
                not_mentioned_count: 6
                open_count: 2
                paused_open_count: 0
                state: problems
                window_days: 30
              schema:
                $ref: '#/components/schemas/AnswerAccuracySummaryResponse'
          description: Counts over the last 30 days and the state they add up to.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              example:
                code: FEATURE_DISABLED
                error: Answer accuracy is not available yet.
          description: Answer accuracy is not switched on yet
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get answer accuracy summary
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/summary\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/summary\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/answer-accuracy/summary\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/brand-aliases:
    get:
      description: 'Returns the brand-name merges configured for this project''s workspace.


        Brand mentions are reported under a canonical name, so two spellings of

        one brand are counted together rather than competing with each other in

        the rankings. This endpoint is how you find out which names were merged

        and into what. Without it, a consumer joining `competitors/mentions` or

        `competitors/trends` on brand name cannot tell a merged name from a raw

        one, and cannot reproduce the aggregate.


        Each group is one canonical name plus the variants folded into it.

        `variants` holds the **normalised fingerprint** of each merged variant -

        lowercased and whitespace-collapsed - because the original casing is

        not retained. Note that fingerprinting preserves punctuation, so

        `Acme Inc.` and `Acme Inc` are distinct variants unless one was

        explicitly merged.


        **This list is not the whole merge story.** A standard dictionary of

        well-known brand-name variants also applies, behind these

        workspace-level entries, and it is not enumerable through the API. So

        two names can be reported together without appearing here. What this

        endpoint tells you exactly is which merges *your workspace* configured,

        and those take precedence: where a workspace entry and the standard

        dictionary both cover a name, the workspace entry is the one that wins.


        Alias rows are workspace-scoped rather than per project, so the same

        map applies to every project in the workspace. It is configuration

        rather than a measurement, so it is not windowed and takes no

        `date_from` or `date_to`, and `engines`, `tag_ids` or `url_ids` are

        rejected with 422. Use `q` for a case-insensitive

        substring match on `canonical_name`, and `sort` on `canonical_name` or

        `created_at`.


        The map is read-only here. Changing a merge changes what future

        rankings mean, so it stays an in-app action.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `VALIDATION_ERROR` | 422 | `engines`, `tag_ids` or `url_ids` was supplied,
        or `sort` named a field other than `canonical_name` or `created_at` |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: brand_aliases_v1_projects__project_id__brand_aliases_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - canonical_name: Acme Analytics
                  created_at: '2026-03-11T09:14:00Z'
                  variants:
                  - acme analytics inc
                  - acme analytics co
                - canonical_name: Rival Analytics
                  created_at: '2026-04-02T16:40:00Z'
                  variants:
                  - rival analytics ltd
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/BrandAliasListResponse'
          description: Paginated list of the brand-name merges configured for this
            project's workspace. `total` counts the alias groups reachable by paging
            this endpoint, so `ceil(total / limit)` is the page count.
        '422':
          content:
            application/json:
              example:
                code: VALIDATION_ERROR
                error: filtering brand aliases by tag_ids is not supported; the alias
                  map is configuration and is not scoped by engine, tag or URL
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: A filter that does not apply to the alias map was supplied
      summary: List brand aliases
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/brand-aliases?limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/brand-aliases?limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/brand-aliases\"\
          ,\n    params={\"limit\": 50},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/brand-facts:
    get:
      description: 'Returns the brand facts registered for a project: short statements

        about your own business, such as a price or a plan, that ziptie.ai

        compares AI answers with. ziptie.ai uses an AI model to make that

        comparison. It can be wrong: dismiss a problem that is not one, or

        edit the fact.


        A project holds at most `max_facts` facts (15), paused ones included,

        so the whole list is small. `status` keeps `active` or `paused` facts

        and `category` keeps one category; `total` counts the facts under

        those filters. `sort` takes `created_at` (default, `created_at:asc`)

        or `category`. `problem_status` is the status of the fact''s problem,

        or null when it has none.


        The list is project configuration, not a measurement, so it takes no

        `date_from`, `date_to`, `engines`, `tag_ids`, `url_ids` or `q`; they

        are refused with 422 `VALIDATION_ERROR`. Part of paid plans: a trial

        has no API access, and a request whose plan cannot be confirmed as

        paid gets `402 PAID_PLAN_REQUIRED`. A workspace whose paid plan has

        lapsed keeps reading.


        See [Pagination](/content/guides/pagination.html) and

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |

        | `VALIDATION_ERROR` | 422 | A filter that does not apply was sent, or `sort`
        named a field other than `created_at` or `category` |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_brand_facts_v1_projects__project_id__brand_facts_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Only facts with this status: `active` or `paused`.'
        in: query
        name: status
        required: false
        schema:
          anyOf:
          - enum:
            - active
            - paused
            type: string
          - type: 'null'
          description: 'Only facts with this status: `active` or `paused`.'
          title: Status
      - description: 'Only facts in this category: `price`, `plan`, `feature`, `location`
          or `other`.'
        in: query
        name: category
        required: false
        schema:
          anyOf:
          - enum:
            - price
            - plan
            - feature
            - location
            - other
            type: string
          - type: 'null'
          description: 'Only facts in this category: `price`, `plan`, `feature`, `location`
            or `other`.'
          title: Category
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - category: price
                  created_at: '2026-10-01T10:00:00+00:00'
                  id: b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40
                  problem_status: open
                  source: settings
                  statement: The Pro plan costs 49 USD a month.
                  status: active
                  updated_at: '2026-10-12T09:30:00+00:00'
                  version: 2
                - category: location
                  created_at: '2026-10-03T14:00:00+00:00'
                  id: 0c4d7f2e-1a6b-4e83-8b95-7f3a2d9c5e61
                  problem_status: null
                  source: wizard
                  statement: We ship from Rotterdam.
                  status: paused
                  updated_at: '2026-10-03T14:00:00+00:00'
                  version: 1
                limit: 50
                max_facts: 15
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/BrandFactListResponse'
          description: The project's brand facts, offset paged.
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              example:
                code: FEATURE_DISABLED
                error: Answer accuracy is not available yet.
          description: Answer accuracy is not switched on yet
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List brand facts
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/brand-facts?status=active\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/brand-facts?status=active\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/brand-facts\"\
          ,\n    params={\"status\": \"active\"},\n    headers={\"Authorization\"\
          : \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
    post:
      description: 'Registers one brand fact: a single claim about your own business,
        such

        as "The Pro plan costs 49 USD a month." (at most 200 characters), with

        a `category`. ziptie.ai then compares new AI answers with it. ziptie.ai

        uses an AI model to make that comparison. It can be wrong: dismiss a

        problem that is not one, or edit the fact. Do not enter personal data.


        A project holds at most 15 facts, paused ones included. The write

        lands in the app as the app''s own write does, recorded under the

        actor `api`, and the fact is checked against answers captured from

        then on. Needs the `admin` token scope, as the app restricts fact

        edits to project admins.


        Send a unique `Idempotency-Key` header to make the call safely

        retryable; replays return the cached response with

        `X-Idempotent-Replay: true`. Only a successful answer is stored and

        replayed; a refused request is not, so a retry with the same key runs

        it again. See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |

        | `BRAND_FACT_LIMIT_REACHED` | 409 | The project already holds 15 facts. Delete
        or edit one |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |

        | `BRAND_FACT_INVALID` | 422 | The statement is empty, too long, or not a
        single claim about your business |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: create_brand_fact_v1_projects__project_id__brand_facts_post
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              category: price
              statement: The Pro plan costs 49 USD a month.
            schema:
              $ref: '#/components/schemas/CreateBrandFactBody'
        required: true
      responses:
        '201':
          content:
            application/json:
              example:
                category: price
                created_at: '2026-10-27T10:00:00+00:00'
                id: b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40
                problem_status: null
                source: settings
                statement: The Pro plan costs 49 USD a month.
                status: active
                updated_at: '2026-10-27T10:00:00+00:00'
                version: 1
              schema:
                $ref: '#/components/schemas/BrandFactCreateResponse'
          description: The newly created brand fact.
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              example:
                code: FEATURE_DISABLED
                error: Answer accuracy is not available yet.
          description: Answer accuracy is not switched on yet
        '409':
          content:
            application/json:
              examples:
                brand_fact_limit_reached:
                  summary: The project already holds 15 facts
                  value:
                    code: BRAND_FACT_LIMIT_REACHED
                    error: A project holds at most 15 brand facts.
                idempotency_key_reused:
                  summary: Same Idempotency-Key with different body
                  value:
                    code: IDEMPOTENCY_KEY_REUSED
                    error: The provided Idempotency-Key has already been used with
                      a different request body.
          description: Conflict - the fact limit is reached, or the idempotency key
            was reused
        '422':
          content:
            application/json:
              example:
                code: BRAND_FACT_INVALID
                error: A fact is one claim about your business, at most 200 characters.
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: The statement is not a usable fact
      summary: Create brand fact
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/brand-facts\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          statement\":\"The Pro plan costs 49 USD a month.\",\"category\":\"price\"\
          }'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/brand-facts\"\
          , {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n    statement: \"\
          The Pro plan costs 49 USD a month.\",\n    category: \"price\",\n  }),\n\
          });\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/brand-facts\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"statement\"\
          : \"The Pro plan costs 49 USD a month.\", \"category\": \"price\"},\n)\n\
          data = res.json()\n"
      x-required-scope: admin
  /v1/projects/{project_id}/brand-facts/{fact_id}:
    delete:
      description: 'Permanently deletes a brand fact together with its versions, its

        verdicts and its problem with that problem''s history. The operation is

        idempotent: deleting a fact that is already gone, or an id the project

        does not hold, still returns 204 with no body. Needs the `admin` token

        scope. ziptie.ai uses an AI model to compare answers with your facts.

        It can be wrong: dismiss a problem that is not one, or edit the fact.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: delete_brand_fact_v1_projects__project_id__brand_facts__fact_id__delete
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the brand fact.
        in: path
        name: fact_id
        required: true
        schema:
          description: Unique identifier of the brand fact.
          maxLength: 64
          minLength: 1
          title: Fact Id
          type: string
      responses:
        '204':
          description: Empty response on successful deletion.
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              example:
                code: FEATURE_DISABLED
                error: Answer accuracy is not available yet.
          description: Answer accuracy is not switched on yet
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Delete brand fact
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X DELETE \"https://api.ziptie.ai/v1/projects/abc123/brand-facts/b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/brand-facts/b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40\"\
          ,\n  { method: \"DELETE\", headers: { Authorization: \"Bearer zt_live_...\"\
          \ } }\n);\n// 204 No Content\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.delete(\n    \"https://api.ziptie.ai/v1/projects/abc123/brand-facts/b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\n# 204 No\
          \ Content\n"
      x-required-scope: admin
    patch:
      description: 'Partially updates a brand fact. Only the fields you send change:
        pass

        `statement` or `category` to rewrite the fact, `status` to pause it

        (`paused` facts are not checked) or resume it (`active`). At least one

        of the three must be sent or the request returns 422.


        Changing the statement or the category writes a new `version`; a

        status change alone does not. A problem open on the fact becomes

        `fact_changed` when the version changes, and nothing is re-checked: a

        later answer that contradicts the new version reopens it. ziptie.ai

        uses an AI model to compare answers with your facts. It can be wrong:

        dismiss a problem that is not one, or edit the fact.


        The response is the fact as stored after the update. Needs the

        `admin` token scope. Send an `Idempotency-Key` header to make the call

        safely retryable. Only a successful answer is stored and replayed. See

        [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; answer accuracy needs a paid plan |

        | `NOT_FOUND` | 404 | The fact does not exist or belongs to a different project
        |

        | `FEATURE_DISABLED` | 404 | Answer accuracy is not switched on yet |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |

        | `BRAND_FACT_INVALID` | 422 | The new statement is empty, too long, or not
        a single claim about your business |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: update_brand_fact_v1_projects__project_id__brand_facts__fact_id__patch
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the brand fact.
        in: path
        name: fact_id
        required: true
        schema:
          description: Unique identifier of the brand fact.
          maxLength: 64
          minLength: 1
          title: Fact Id
          type: string
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              statement: The Pro plan costs 59 USD a month.
            schema:
              $ref: '#/components/schemas/PatchBrandFactBody'
        required: true
      responses:
        '200':
          content:
            application/json:
              example:
                category: price
                created_at: '2026-10-01T10:00:00+00:00'
                id: b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40
                problem_status: fact_changed
                source: settings
                statement: The Pro plan costs 59 USD a month.
                status: active
                updated_at: '2026-10-27T11:00:00+00:00'
                version: 2
              schema:
                $ref: '#/components/schemas/BrandFactUpdateResponse'
          description: The brand fact as stored after the update.
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Answer accuracy is on every paid plan.
          description: The workspace's plan cannot be confirmed as paid; answer accuracy
            needs a paid plan
        '404':
          content:
            application/json:
              examples:
                feature_disabled:
                  summary: Answer accuracy is not switched on yet
                  value:
                    code: FEATURE_DISABLED
                    error: Answer accuracy is not available yet.
                not_found:
                  summary: The fact is not in this project
                  value:
                    code: NOT_FOUND
                    error: Brand fact not found
          description: The fact is not in this project, or answer accuracy is not
            switched on yet
        '409':
          content:
            application/json:
              example:
                code: IDEMPOTENCY_KEY_REUSED
                error: The provided Idempotency-Key has already been used with a different
                  request body.
          description: Conflict - the idempotency key was reused with a different
            body
        '422':
          content:
            application/json:
              example:
                code: BRAND_FACT_INVALID
                error: A fact is one claim about your business, at most 200 characters.
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: The new statement is not a usable fact
      summary: Update brand fact
      tags:
      - Answer accuracy
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X PATCH \"https://api.ziptie.ai/v1/projects/abc123/brand-facts/b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          statement\":\"The Pro plan costs 59 USD a month.\"}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/brand-facts/b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40\"\
          ,\n  {\n    method: \"PATCH\",\n    headers: {\n      Authorization: \"\
          Bearer zt_live_...\",\n      \"Content-Type\": \"application/json\",\n \
          \     \"Idempotency-Key\": crypto.randomUUID(),\n    },\n    body: JSON.stringify({\
          \ statement: \"The Pro plan costs 59 USD a month.\" }),\n  }\n);\nconst\
          \ data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.patch(\n    \"https://api.ziptie.ai/v1/projects/abc123/brand-facts/b8e2c3a1-4f5d-4c7e-9a10-2d6f8e1b3c40\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"statement\"\
          : \"The Pro plan costs 59 USD a month.\"},\n)\ndata = res.json()\n"
      x-required-scope: admin
  /v1/projects/{project_id}/checks:
    get:
      description: 'Returns the project''s whole check history, one row per (prompt,
        check),

        newest first. This is the bulk-read surface: the per-prompt endpoint

        `prompts/{prompt_id}/checks` serves one prompt''s slice, so assembling a

        project''s history through it costs one request per prompt plus paging

        inside each. This endpoint replaces that fan-out with ordinary paging.


        Each row carries `prompt_id`, which a project-wide row needs. Since

        June 2026 a `check_no` is one project check cycle, so every prompt

        answered in that check shares it; before June 2026 each prompt numbered

        its own checks, so two older rows can share a number without being

        related. Join on `prompt_id` to reach the prompt, and on

        `(prompt_id, check_no)` to fetch the full evidence from

        `prompts/{prompt_id}/checks/{check_no}`, or page the answers of many

        prompts at once from `checks/answers`.


        **Sorting is real here and matters for syncing.** `sort=timestamp:asc`

        walks the history oldest-first, so an incremental sync can resume from

        the last row it stored instead of re-reading from the newest. Allowed

        sort fields are `timestamp` and `prompt_id`. `check_no` is deliberately

        not sortable: over history from before June 2026, when each prompt

        numbered its own checks, it interleaves unrelated histories.


        **Failed checks.** An engine whose run of a check failed appears with

        `failed` true and null `cited` and `mentioned`, and is left out of the

        check''s `success_score`. When no engine answered a check, its

        `success_score` is `null` and `answered_checks` is 0.


        `engines` narrows which engine tables are consulted. A check produced

        only by an excluded engine disappears from `data` **and** from `total`,

        matching the per-prompt endpoint so the two agree for the same filter.

        A check is in the window when one of its engine answers was recorded on

        a day of it (UTC), and `applied` echoes the window and engines read.

        `tag_ids` and `url_ids` are rejected rather than accepted and ignored,

        because a check is recorded against a prompt and is not scoped by

        either. See

        [Filtering and sorting](/content/guides/filtering-and-sorting.html) and

        [Pagination](/content/guides/pagination.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `tag_ids` or `url_ids` was supplied, or `sort`
        named a field other than `timestamp` or `prompt_id` |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_project_checks_v1_projects__project_id__checks_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-01-01'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                data:
                - answered_checks: 2
                  check_no: 18
                  engines:
                    chatgpt:
                      cited: true
                      failed: false
                      mentioned: true
                    google_aio:
                      cited: false
                      failed: false
                      mentioned: true
                  failed_checks: 0
                  prompt_id: 4412
                  success_score: 0.72
                  timestamp: '2026-04-19T06:00:00Z'
                - answered_checks: 1
                  check_no: 18
                  engines:
                    chatgpt:
                      cited: false
                      failed: false
                      mentioned: false
                    google_aio:
                      cited: null
                      failed: true
                      mentioned: null
                  failed_checks: 1
                  prompt_id: 4407
                  success_score: 0.35
                  timestamp: '2026-04-19T06:00:00Z'
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/ProjectCheckListResponse'
          description: Paginated check history across every prompt in the project,
            newest first. `total` counts the (prompt, check) pairs reachable by paging
            this endpoint, so `ceil(total / limit)` is the page count.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date_to - date_from exceeds the maximum allowed range of
                      365 days.
                unsupported_filter:
                  summary: A filter that does not scope a check
                  value:
                    code: VALIDATION_ERROR
                    error: filtering project checks by tag_ids is not supported; a
                      check is recorded against a prompt and is not scoped by tag
                      or URL
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a filter that does not apply
      summary: List project checks
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/checks?date_from=2026-01-01&date_to=2026-04-21&sort=timestamp:asc&limit=100\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/checks?sort=timestamp:asc&limit=100\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/checks\"\
          ,\n    params={\"date_from\": \"2026-01-01\", \"date_to\": \"2026-04-21\"\
          ,\n            \"sort\": \"timestamp:asc\", \"limit\": 100},\n    headers={\"\
          Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/checks/answers:
    get:
      description: "Returns what the engines actually said, for many prompts at once:\
        \ one\nitem per (prompt, check) pair, carrying every engine's full answer\
        \ text,\nthe brand and citation flags, the sentiment, every brand the answer\n\
        named with the facts stated about it, and the sources the engine cited.\n\
        It is the bulk counterpart of `prompts/{prompt_id}/checks/{check_no}`,\nwhich\
        \ reads one prompt's check per request.\n\n**Pick exactly one selection per\
        \ request:**\n\n- `latest=true` - each prompt's own latest check.\n- `check_no=N`\
        \ - one check: every prompt's answers numbered `N`.\n- a `date_from` / `date_to`\
        \ window - every check with an answer inside\n  the window, at most 365 days.\
        \ This is the selection when neither\n  `latest` nor `check_no` is given,\
        \ and its default is 30 days before\n  today through today (UTC). A `date_to`\
        \ later than today reads as\n  today.\n\nCombining `latest` with `check_no`\
        \ or with a date, or `check_no` with a\ndate, is refused with 422 `VALIDATION_ERROR`.\
        \ `engines`, `tag_ids`\n(any-of, at most 100) and `prompt_id` narrow any of\
        \ the three.\n\n**This endpoint pages by cursor, the only one on `/v1/` that\
        \ does.**\nItems come in a fixed `(prompt_id, check_no)` order, so there is\
        \ no\n`sort`, no `offset` and no `total`. Pass each page's `next_cursor` back\n\
        as `cursor`, and stop only when `next_cursor` is `null`: a page can hold\n\
        fewer than `limit` items, or none, while more follow. `limit` is 1-25\nand\
        \ defaults to 5, because one pair carries up to seven full answers.\n\n**In\
        \ window mode, pass explicit `date_from` and `date_to` on every page\nof a\
        \ walk.** The default window is recomputed from today's UTC date on\neach\
        \ request, so a walk that crosses midnight UTC without explicit\ndates silently\
        \ gains a day at the new end and loses its oldest. See\n[Pagination](/content/guides/pagination.html).\n\
        \n`applied` echoes the window read, `null` dates under `latest` or\n`check_no`,\
        \ and the engines the answers cover.\n\n**What a check number means.** Since\
        \ June 2026 a check number is one\nproject check cycle. A scheduled cycle\
        \ opens when the project is due. A\nprompt created before the project is next\
        \ due joins the open cycle,\nwhich on a weekly or monthly project can be days\
        \ after it opened, and\nan on-demand recheck opens a cycle that covers only\
        \ the prompts it\nrechecked. So one check's answers can carry different dates,\
        \ and one\ncheck need not cover every prompt. Before June 2026 each prompt\
        \ numbered\nits own checks, so `check_no=N` over older history returns each\n\
        prompt's own Nth check, whatever its date. Every item carries its\n`timestamp`,\
        \ the time of its earliest answer.\n\nEngine keys are the public names, and\
        \ an engine with no row for a pair\nis `null` rather than absent. An engine\
        \ whose run failed is present\nwith `failed` true, an empty `response_text`\
        \ and null flags, and is\nleft out of `success_score`. When no engine answered\
        \ a pair, its\n`success_score` is `null` and `answered_checks` is 0. The raw\
        \ failure\ntext of an engine run is not published.\nSee [Engine names](/content/guides/engine-names.html).\n\
        \nEach item also carries `google_organic_position` and\n`bing_organic_position`:\
        \ the best organic rank (1 is the top result) a\nURL from the project's domain\
        \ held at that check. Google organic\nresults are captured by `google_aio`\
        \ checks and Bing organic results by\n`bing_ai_overview` checks, so each rank\
        \ is `null` unless its engine is\nrequested: `engines=chatgpt` returns both\
        \ as `null`. Either is also\n`null` when the domain did not rank in the top\
        \ results or the check has\nno organic data. Both keys are always present.\
        \ With `check_no=N` this\nlists every prompt's ranks at that one check.\n\n\
        **Errors specific to this endpoint:**\n\n| Code | HTTP | When |\n|------|------|------|\n\
        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist\
        \ |\n| `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace's\
        \ plan does not include |\n| `NOT_FOUND` | 404 | `prompt_id` is not a prompt\
        \ of this project, or no answer in this project carries `check_no` |\n| `VALIDATION_ERROR`\
        \ | 422 | More than one selection, `limit` outside 1-25, a `check_no`, `tag_ids`\
        \ or `prompt_id` outside 1-2147483647, more than 100 `tag_ids`, or a malformed\
        \ `cursor` |\n| `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds\
        \ 365 days |\n\nSee [error codes](/content/error-codes.html) for the standard\
        \ errors (401, 402, 403, 422, 429, 500, 503).\n"
      operationId: list_check_answers_v1_projects__project_id__checks_answers_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Return each prompt's own latest check. Cannot be combined with
          `check_no`, `date_from` or `date_to`.
        in: query
        name: latest
        required: false
        schema:
          default: false
          description: Return each prompt's own latest check. Cannot be combined with
            `check_no`, `date_from` or `date_to`.
          title: Latest
          type: boolean
      - description: 'Return one check: every prompt''s answers carrying this check
          number. Cannot be combined with `latest` or a date.'
        in: query
        name: check_no
        required: false
        schema:
          anyOf:
          - maximum: 2147483647
            minimum: 1
            type: integer
          - type: 'null'
          description: 'Return one check: every prompt''s answers carrying this check
            number. Cannot be combined with `latest` or a date.'
          title: Check No
      - description: Inclusive `YYYY-MM-DD` start of the window (UTC), read when neither
          `latest` nor `check_no` is given. Defaults to 30 days before today (UTC);
          pass it explicitly on every page of a walk.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive `YYYY-MM-DD` start of the window (UTC), read when
            neither `latest` nor `check_no` is given. Defaults to 30 days before today
            (UTC); pass it explicitly on every page of a walk.
          title: Date From
      - description: Inclusive `YYYY-MM-DD` end of the window (UTC). Defaults to today,
          and a later day reads as today; pass it explicitly on every page of a walk.
          A window spans at most 365 days.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive `YYYY-MM-DD` end of the window (UTC). Defaults to
            today, and a later day reads as today; pass it explicitly on every page
            of a walk. A window spans at most 365 days.
          title: Date To
      - description: Comma-separated public engine names (e.g. 'chatgpt,perplexity').
          Omit for every engine on the plan. See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated public engine names (e.g. 'chatgpt,perplexity').
            Omit for every engine on the plan. See Engine names guide.
          title: Engines
      - description: Comma-separated tag IDs; a prompt matches when it carries any
          of them. At most 100, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated tag IDs; a prompt matches when it carries any
            of them. At most 100, each 1-2147483647.
          title: Tag Ids
      - description: Read one prompt of the project only.
        in: query
        name: prompt_id
        required: false
        schema:
          anyOf:
          - maximum: 2147483647
            minimum: 1
            type: integer
          - type: 'null'
          description: Read one prompt of the project only.
          title: Prompt Id
      - description: 'Opaque cursor: the previous page''s `next_cursor`. Omit on the
          first page.'
        in: query
        name: cursor
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Opaque cursor: the previous page''s `next_cursor`. Omit on
            the first page.'
          title: Cursor
      - description: Pairs per page, 1-25. Defaults to 5.
        in: query
        name: limit
        required: false
        schema:
          default: 5
          description: Pairs per page, 1-25. Defaults to 5.
          maximum: 25
          minimum: 1
          title: Limit
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-08-22'
                  date_to: '2026-09-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                data:
                - answered_checks: 2
                  bing_organic_position: null
                  check_no: 18
                  engines:
                    bing_ai_overview: null
                    chatgpt:
                      brand_mention: true
                      brands:
                      - brand: MyBrand
                        details: []
                        order_of_mention: 2
                        prominence: medium
                        sentiment: neutral
                      citations: true
                      failed: false
                      favored_brand: null
                      followup_queries: null
                      response_length: 2410
                      response_status: null
                      response_text: Several tools cover this. MyBrand is often cited
                        for...
                      sentiment: Mixed
                      sources:
                      - domain: mybrand.com
                        position: 1
                        source_kind: web
                        source_title: null
                        url: https://mybrand.com/agencies
                      subtopics: null
                    copilot: null
                    gemini: null
                    google_ai_mode: null
                    google_aio:
                      brand_mention: true
                      brands:
                      - brand: MyBrand
                        details:
                        - detail: White-label client reports
                          sentiment: positive
                        order_of_mention: 1
                        prominence: high
                        sentiment: positive
                      citations: false
                      failed: false
                      favored_brand: MyBrand
                      followup_queries:
                      - Which rank tracker has white-label reports?
                      response_length: 1842
                      response_status: true
                      response_text: Agencies usually pick a tracker that covers AI
                        answers as well as rankings...
                      sentiment: Positive
                      sources:
                      - domain: competitor.com
                        position: 1
                        source_kind: web
                        source_title: The best rank trackers for agencies
                        url: https://competitor.com/blog/rank-trackers
                      subtopics:
                      - reporting
                      - pricing
                    perplexity:
                      brand_mention: null
                      brands: []
                      citations: null
                      failed: true
                      favored_brand: null
                      followup_queries: null
                      response_length: 0
                      response_status: null
                      response_text: ''
                      sentiment: null
                      sources: []
                      subtopics: null
                  failed_checks: 1
                  google_organic_position: 4
                  prompt_id: 4407
                  prompt_text: best rank tracker for agencies
                  success_score: 0.5
                  tags:
                  - color: '#0f766e'
                    id_tag: 12
                    name: Pricing
                  timestamp: '2026-09-21T06:00:04Z'
                limit: 5
                next_cursor: '4407:18'
              schema:
                $ref: '#/components/schemas/CheckAnswersListResponse'
          description: One cursor page of full engine answers, one item per (prompt,
            check) pair, in (prompt_id, check_no) order.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '404':
          content:
            application/json:
              examples:
                check_not_found:
                  summary: No answer in this project carries check_no
                  value:
                    code: NOT_FOUND
                    error: Check not found in project
                prompt_not_found:
                  summary: prompt_id is not a prompt of this project
                  value:
                    code: NOT_FOUND
                    error: Prompt not found in project
          description: The prompt is not in this project, or no answer carries the
            check number
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                malformed_cursor:
                  summary: A cursor this endpoint did not issue
                  value:
                    code: VALIDATION_ERROR
                    error: cursor is malformed
                mixed_selection:
                  summary: Two selections in one request
                  value:
                    code: VALIDATION_ERROR
                    error: latest and check_no cannot be combined; pass exactly one
                      selection
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: More than one selection, a value out of range, or a malformed
            cursor
      summary: List check answers
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "# Walk a window: the same explicit dates on every page, until next_cursor\
          \ is null.\ncursor=\"\"\nwhile :; do\n  page=$(curl -s -G \"https://api.ziptie.ai/v1/projects/abc123/checks/answers\"\
          \ \\\n    -H \"Authorization: Bearer zt_live_...\" \\\n    --data-urlencode\
          \ \"date_from=2026-09-01\" \\\n    --data-urlencode \"date_to=2026-09-21\"\
          \ \\\n    --data-urlencode \"limit=25\" \\\n    ${cursor:+--data-urlencode\
          \ \"cursor=$cursor\"})\n  echo \"$page\" | jq -c '.data[]'\n  cursor=$(echo\
          \ \"$page\" | jq -r '.next_cursor // empty')\n  [ -z \"$cursor\" ] && break\n\
          done\n"
      - label: Node
        lang: JavaScript
        source: "const url = \"https://api.ziptie.ai/v1/projects/abc123/checks/answers\"\
          ;\n// The same explicit dates on every page: the default window moves with\
          \ the server's date.\nconst params = { date_from: \"2026-09-01\", date_to:\
          \ \"2026-09-21\", limit: \"25\" };\nconst items = [];\nlet cursor = null;\n\
          do {\n  const query = new URLSearchParams(cursor ? { ...params, cursor }\
          \ : params);\n  const res = await fetch(`${url}?${query}`, {\n    headers:\
          \ { Authorization: \"Bearer zt_live_...\" },\n  });\n  const page = await\
          \ res.json();\n  items.push(...page.data); // a page can be short or empty\
          \ mid-walk\n  cursor = page.next_cursor;\n} while (cursor !== null);\n"
      - label: Python
        lang: Python
        source: "import requests\n\nurl = \"https://api.ziptie.ai/v1/projects/abc123/checks/answers\"\
          \n# The same explicit dates on every page: the default window moves with\
          \ the server's date.\nparams = {\"date_from\": \"2026-09-01\", \"date_to\"\
          : \"2026-09-21\", \"limit\": 25}\nitems, cursor = [], None\nwhile True:\n\
          \    page = requests.get(\n        url,\n        params={**params, \"cursor\"\
          : cursor} if cursor else params,\n        headers={\"Authorization\": \"\
          Bearer zt_live_...\"},\n    ).json()\n    items.extend(page[\"data\"]) \
          \ # a page can be short or empty mid-walk\n    cursor = page[\"next_cursor\"\
          ]\n    if cursor is None:\n        break\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitor-tags:
    get:
      description: 'Returns the project''s competitor tags, paginated and sorted.
        A

        competitor tag is a label with a name and a colour that groups the

        brands and domains you compete with, such as "Direct competitors" or

        "Local market", so the competitor rankings can compare you against

        each group with `group_by=tag`. Competitor tags are separate from

        prompt tags: they never appear under `GET /tags`, and `tag_ids` keeps

        filtering by prompt tags everywhere.


        `domain_count` and `brand_count` count the visible members of each

        tag: brands and domains hidden in the app and the project''s own brands

        and domains are not counted. Read the members with

        `GET /competitor-tags/{tag_id}/members`.


        The list is project configuration rather than a measurement, so it

        takes no `date_from` or `date_to`, and `engines`, `tag_ids` or

        `url_ids` are rejected with a 422 rather than silently dropped. Use `q`

        for a case-insensitive substring match on `name`, and `sort` on

        `name`, `domain_count` or `brand_count` (default `name:asc`). See

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `VALIDATION_ERROR` | 422 | `engines`, `tag_ids` or `url_ids` was supplied,
        or `sort` named a field other than `name`, `domain_count` or `brand_count`
        |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_competitor_tags_v1_projects__project_id__competitor_tags_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - brand_count: 1
                  color: '#2563EB'
                  domain_count: 1
                  id: 42
                  name: Direct competitors
                - brand_count: 0
                  color: '#DC2626'
                  domain_count: 0
                  id: 43
                  name: Local market
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorTagListResponse'
          description: Paginated list of the project's competitor tags.
        '422':
          content:
            application/json:
              example:
                code: VALIDATION_ERROR
                error: filtering competitor tags by 'tag_ids' is not supported; competitor
                  tags are project configuration and are not scoped by engine, prompt
                  tag or URL
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: A filter that does not apply to competitor tags was supplied
      summary: List competitor tags
      tags:
      - Competitor tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags?limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags?limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags\"\
          ,\n    params={\"limit\": 50},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
    post:
      description: 'Creates a competitor tag with a display `name` and an optional

        `#RRGGBB` hex `color`. The name is trimmed and must then be 1 to 120

        characters; it is unique in the project ignoring case, and may equal

        the name of a prompt tag, because the two kinds of tag never mix. Omit

        `color` to take the next colour of the competitor tag palette. A new

        tag has no members, so both counts are 0; add brands and domains with

        `POST /competitor-tags/{tag_id}/members`. Competitor tags have no plan

        cap.


        Send a unique `Idempotency-Key` header to make the call safely

        retryable; replays return the cached response with

        `X-Idempotent-Replay: true`. See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `COMPETITOR_TAG_ALREADY_EXISTS` | 409 | A competitor tag with the same name,
        ignoring case, already exists in this project |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |

        | `VALIDATION_ERROR` | 422 | The name is empty after trimming or longer than
        120 characters, the colour is not `#RRGGBB`, or the body carries an unknown
        field |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: create_competitor_tag_v1_projects__project_id__competitor_tags_post
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              color: '#DC2626'
              name: Local market
            schema:
              $ref: '#/components/schemas/CreateCompetitorTagBody'
        required: true
      responses:
        '201':
          content:
            application/json:
              example:
                brand_count: 0
                color: '#DC2626'
                domain_count: 0
                id: 43
                name: Local market
              schema:
                $ref: '#/components/schemas/CompetitorTagCreateResponse'
          description: The newly created competitor tag.
        '409':
          content:
            application/json:
              examples:
                competitor_tag_already_exists:
                  summary: Duplicate competitor tag name
                  value:
                    code: COMPETITOR_TAG_ALREADY_EXISTS
                    error: A competitor tag with this name already exists
                idempotency_key_reused:
                  summary: Same Idempotency-Key with different body
                  value:
                    code: IDEMPOTENCY_KEY_REUSED
                    error: Idempotency key reused with different body
          description: Conflict - duplicate competitor tag name or idempotency-key
            reuse
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Create competitor tag
      tags:
      - Competitor tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          name\":\"Local market\",\"color\":\"#DC2626\"}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/competitor-tags\"\
          , {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({ name: \"Local market\"\
          , color: \"#DC2626\" }),\n});\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"name\"\
          : \"Local market\", \"color\": \"#DC2626\"},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/competitor-tags/{tag_id}:
    delete:
      description: 'Permanently deletes a competitor tag and removes it from every
        brand

        and domain that carried it. A brand or domain left with no competitor

        tag stops being a tracked competitor and leaves `GET /competitors`;

        one that still carries another tag stays. The operation is idempotent:

        deleting a competitor tag that does not exist still returns 204 with

        no body.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: delete_competitor_tag_v1_projects__project_id__competitor_tags__tag_id__delete
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the competitor tag.
        in: path
        name: tag_id
        required: true
        schema:
          description: Unique identifier of the competitor tag.
          maximum: 2147483647
          minimum: 1
          title: Tag Id
          type: integer
      responses:
        '204':
          description: Empty response on successful deletion.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Delete competitor tag
      tags:
      - Competitor tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X DELETE \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42\"\
          , {\n  method: \"DELETE\",\n  headers: { Authorization: \"Bearer zt_live_...\"\
          \ }\n});\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.delete(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\n"
      x-required-scope: admin
    patch:
      description: 'Renames or recolours a competitor tag. Only the fields you send
        are

        changed; at least one of `name` and `color` must be provided or the

        request returns 422. The name follows the create rules: 1 to 120

        characters after trimming, unique in the project ignoring case. The

        response is the tag''s state after the update, and every brand and

        domain carrying the tag keeps it.


        Send an `Idempotency-Key` header to make the call safely retryable.

        See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | The competitor tag does not exist or belongs to a different
        project |

        | `COMPETITOR_TAG_ALREADY_EXISTS` | 409 | Another competitor tag in this project
        already has the new name, ignoring case |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: update_competitor_tag_v1_projects__project_id__competitor_tags__tag_id__patch
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the competitor tag.
        in: path
        name: tag_id
        required: true
        schema:
          description: Unique identifier of the competitor tag.
          maximum: 2147483647
          minimum: 1
          title: Tag Id
          type: integer
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              name: Direct
            schema:
              $ref: '#/components/schemas/PatchCompetitorTagBody'
        required: true
      responses:
        '200':
          content:
            application/json:
              example:
                color: '#2563EB'
                id: 42
                name: Direct
              schema:
                $ref: '#/components/schemas/CompetitorTagUpdateResponse'
          description: The competitor tag after the update.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Competitor tag not found
          description: Competitor tag not found in this project
        '409':
          content:
            application/json:
              examples:
                competitor_tag_already_exists:
                  summary: New name conflicts with an existing competitor tag
                  value:
                    code: COMPETITOR_TAG_ALREADY_EXISTS
                    error: A competitor tag with this name already exists
                idempotency_key_reused:
                  summary: Same Idempotency-Key with different body
                  value:
                    code: IDEMPOTENCY_KEY_REUSED
                    error: Idempotency key reused with different body
          description: Conflict - duplicate competitor tag name or idempotency-key
            reuse
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Update competitor tag
      tags:
      - Competitor tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X PATCH \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          name\":\"Direct\"}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42\"\
          , {\n  method: \"PATCH\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({ name: \"Direct\"\
          \ }),\n});\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.patch(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"name\"\
          : \"Direct\"},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/competitor-tags/{tag_id}/members:
    get:
      description: 'Returns the brands and domains carrying a competitor tag, paginated

        and sorted. Members are the visible brands and domains of the tag:

        brands and domains hidden in the app are not members, and the

        project''s own brands and domains never count inside a competitor tag,

        so they are left out. Each member''s `id` is the same id the

        tracked-competitors roster (`GET /competitors`) returns for that brand

        or domain, and the id `DELETE .../members/{member_id}` takes.


        The list is project configuration rather than a measurement, so it

        takes no `date_from` or `date_to`, and `engines`, `tag_ids` or

        `url_ids` are rejected with a 422. Use `q` for a case-insensitive

        substring match on `name`, and `sort` on `name` or `type` (default

        `name:asc`). See [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | The competitor tag does not exist or belongs to a different
        project |

        | `VALIDATION_ERROR` | 422 | `engines`, `tag_ids` or `url_ids` was supplied,
        or `sort` named a field other than `name` or `type` |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_competitor_tag_members_v1_projects__project_id__competitor_tags__tag_id__members_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the competitor tag.
        in: path
        name: tag_id
        required: true
        schema:
          description: Unique identifier of the competitor tag.
          maximum: 2147483647
          minimum: 1
          title: Tag Id
          type: integer
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - domain: acme.com
                  id: '901'
                  name: acme.com
                  type: domain
                - domain: null
                  id: '902'
                  name: Initech
                  type: brand
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorTagMemberListResponse'
          description: Paginated list of the brands and domains carrying the tag.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Competitor tag not found
          description: Competitor tag not found in this project
        '422':
          content:
            application/json:
              example:
                code: VALIDATION_ERROR
                error: filtering competitor tag members by 'tag_ids' is not supported;
                  competitor tags are project configuration and are not scoped by
                  engine, prompt tag or URL
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: A filter that does not apply to competitor tag members was
            supplied
      summary: List competitor tag members
      tags:
      - Competitor tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members?limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members?limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members\"\
          ,\n    params={\"limit\": 50},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
    post:
      description: 'Adds brands and domains to a competitor tag, at most 100 per call.
        A

        `domain` member is a host name such as `acme.com`, not a URL; a

        leading `www.` is ignored. A `brand` member matches an existing

        tracked brand ignoring case and surrounding spaces. A brand or domain

        that already carries the tag keeps it, and one carrying other tags

        keeps those too. The project''s own brands and domains cannot carry a

        competitor tag. Adding a brand or domain hidden in the app unhides it;

        hiding itself stays an in-app action.


        The response lists one member per request item, in request order,

        with the roster id of each. Send a unique `Idempotency-Key` header to

        make the call safely retryable. See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | The competitor tag does not exist or belongs to a different
        project |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |

        | `VALIDATION_ERROR` | 422 | `members` is empty or holds more than 100 items,
        a member is one of the project''s own brands or domains, a domain is a URL
        rather than a host name, a name is empty or longer than 255 characters, or
        the body carries an unknown field |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: create_competitor_tag_members_v1_projects__project_id__competitor_tags__tag_id__members_post
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the competitor tag.
        in: path
        name: tag_id
        required: true
        schema:
          description: Unique identifier of the competitor tag.
          maximum: 2147483647
          minimum: 1
          title: Tag Id
          type: integer
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              members:
              - name: acme.com
                type: domain
              - name: Globex
                type: brand
            schema:
              $ref: '#/components/schemas/AddCompetitorTagMembersBody'
        required: true
      responses:
        '201':
          content:
            application/json:
              example:
                data:
                - domain: acme.com
                  id: '901'
                  name: acme.com
                  type: domain
                - domain: null
                  id: '903'
                  name: Globex
                  type: brand
              schema:
                $ref: '#/components/schemas/CompetitorTagMembersAddResponse'
          description: The members written, one per request item.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Competitor tag not found
          description: Competitor tag not found in this project
        '409':
          content:
            application/json:
              example:
                code: IDEMPOTENCY_KEY_REUSED
                error: Idempotency key reused with different body
          description: Same Idempotency-Key with a different body
        '422':
          content:
            application/json:
              example:
                code: VALIDATION_ERROR
                error: Your own brands and domains cannot carry competitor tags
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: A member was refused
      summary: Add competitor tag members
      tags:
      - Competitor tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          members\":[{\"type\":\"domain\",\"name\":\"acme.com\"},{\"type\":\"brand\"\
          ,\"name\":\"Globex\"}]}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members\"\
          ,\n  {\n    method: \"POST\",\n    headers: {\n      Authorization: \"Bearer\
          \ zt_live_...\",\n      \"Content-Type\": \"application/json\",\n      \"\
          Idempotency-Key\": crypto.randomUUID(),\n    },\n    body: JSON.stringify({\n\
          \      members: [\n        { type: \"domain\", name: \"acme.com\" },\n \
          \       { type: \"brand\", name: \"Globex\" },\n      ],\n    }),\n  }\n\
          );\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"members\"\
          : [\n        {\"type\": \"domain\", \"name\": \"acme.com\"},\n        {\"\
          type\": \"brand\", \"name\": \"Globex\"},\n    ]},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/competitor-tags/{tag_id}/members/{member_id}:
    delete:
      description: 'Removes a competitor tag from one brand or domain, named by the

        member id the members list and the tracked-competitors roster return.

        Other tags on that brand or domain stay. When this was its last

        competitor tag, it stops being a tracked competitor and leaves

        `GET /competitors`. The operation is idempotent: it returns 204 with

        no body also when the tag or the member does not exist.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: delete_competitor_tag_member_v1_projects__project_id__competitor_tags__tag_id__members__member_id__delete
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the competitor tag.
        in: path
        name: tag_id
        required: true
        schema:
          description: Unique identifier of the competitor tag.
          maximum: 2147483647
          minimum: 1
          title: Tag Id
          type: integer
      - description: Identifier of the tracked competitor, as the members list and
          the roster return it.
        in: path
        name: member_id
        required: true
        schema:
          description: Identifier of the tracked competitor, as the members list and
            the roster return it.
          maximum: 2147483647
          minimum: 1
          title: Member Id
          type: integer
      responses:
        '204':
          description: Empty response on successful removal.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Remove competitor tag member
      tags:
      - Competitor tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X DELETE \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members/901\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members/901\"\
          ,\n  { method: \"DELETE\", headers: { Authorization: \"Bearer zt_live_...\"\
          \ } }\n);\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.delete(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitor-tags/42/members/901\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\n"
      x-required-scope: admin
  /v1/projects/{project_id}/competitors:
    get:
      description: 'Returns the roster of brands and domains this project tracks as

        competitors: every brand and domain carrying at least one competitor

        tag. Each row lists its `tags` (id, name and colour, in name order).

        Manage the roster with the competitor tags operations: adding a brand

        or domain to a tag with `POST /competitor-tags/{tag_id}/members` makes

        it a tracked competitor, and removing its last tag takes it off. Row

        ids are the member ids of `competitor-tags/{tag_id}/members`.


        A `domain` row tracks a website and carries the domain in both `name`

        and `domain`; a `brand` row tracks a name and leaves `domain` null.

        Brands and domains hidden in the app carry no tags, so they are not

        listed.


        `competitor_tag_ids` narrows the roster to rows carrying any of the

        given competitor tags. It is unrelated to `tag_ids`, which names prompt

        tags and stays refused here.


        This is project configuration rather than a measurement, so it is not

        scoped by time, prompt tag, URL or engine: it takes no `date_from` or

        `date_to`, and `engines`, `tag_ids` or `url_ids` are rejected with a 422

        rather than silently dropped. Use `q` for a case-insensitive substring

        match on `name`, and `sort` on `name` or `type`. See

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `VALIDATION_ERROR` | 422 | `engines`, `tag_ids` or `url_ids` was supplied,
        `competitor_tag_ids` is empty, not a comma-separated list of integers from
        1 to 2147483647, or holds more than 100 ids, or `sort` named a field other
        than `name` or `type` |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: tracked_v1_projects__project_id__competitors_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Comma-separated competitor tag ids. Returns only tracked competitors
          carrying at least one of them. Distinct from tag_ids, which are prompt tags.
        in: query
        name: competitor_tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated competitor tag ids. Returns only tracked competitors
            carrying at least one of them. Distinct from tag_ids, which are prompt
            tags.
          title: Competitor Tag Ids
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - domain: acme.com
                  id: '901'
                  name: acme.com
                  tags:
                  - color: '#2563EB'
                    id: 42
                    name: Direct competitors
                  type: domain
                - domain: null
                  id: '902'
                  name: Initech
                  tags:
                  - color: '#2563EB'
                    id: 42
                    name: Direct competitors
                  - color: '#DC2626'
                    id: 43
                    name: Local market
                  type: brand
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/TrackedCompetitorListResponse'
          description: Paginated roster of the brands and domains this project tracks
            as competitors. `total` counts the tracked competitors reachable by paging
            this endpoint, so `ceil(total / limit)` is the page count.
        '422':
          content:
            application/json:
              example:
                code: VALIDATION_ERROR
                error: filtering tracked competitors by 'tag_ids' is not supported;
                  the roster is project configuration and is not scoped by engine,
                  tag or URL
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: A filter that does not apply to the roster was supplied
      summary: List tracked competitors
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors?limit=50&competitor_tag_ids=42\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors?limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors\"\
          ,\n    params={\"limit\": 50},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/bing-organic:
    get:
      description: 'Returns competitor domains ranked by their appearance in the top-10

        Bing organic SERP captured alongside Bing AI Overview checks. Each

        appearance represents one (check, position) pair where the domain held

        a top-10 slot in the organic results panel. This is the Bing

        counterpart of `competitors/serp`: request both to compare your

        standing on the two search engines over the same tracked prompts.


        Same response shape as `competitors/serp`, including the top-level

        `hero` block (`avg_top_position`, `featured_snippet_rate` and their

        deltas) describing your own domains rather than the ranked competitors,

        and the same `summary`.


        **Domains hidden in the app are left out**, and `filter`,

        `competitor_tag_ids`, the row flags, `rank` and `summary` work exactly

        as on `competitors/citations`, over the same domain marks. The default

        sort is `last_check_share:desc`.


        The window is exactly `date_from` through `date_to`, inclusive: a check

        counts when its cycle started (UTC) on one of those days, and never the

        day before `date_from`. A `date_to` later than today reads as today. `last_check_*`

        describe the last check on or before `date_to`, and the `hero` deltas

        compare the two halves of that window.


        The data has one engine of origin by construction, so the endpoint

        takes no `engines` filter, exactly as `competitors/serp`, and

        `applied.engines` is null. It takes no `url_ids` or `q` either; any of

        the three sent anyway is dropped like any other parameter the

        operation does not declare. Filter by `tag_ids` to scope to a slice of

        prompts. See

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `filter` is not `all`, `competitors` or `hidden`;
        `competitor_tag_ids` is malformed or holds more than 100 ids; or the `sort`
        field is not one of the allowed ones |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: bing_organic_v1_projects__project_id__competitors_bing_organic_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Which rows to keep, as the app's Competitors page filter does.
          `all` (the default) returns every row except the ones hidden in the app.
          `competitors` keeps the rows marked as competitors. `hidden` keeps only
          the hidden rows. Your own domains or brands are kept in every mode.
        in: query
        name: filter
        required: false
        schema:
          default: all
          description: Which rows to keep, as the app's Competitors page filter does.
            `all` (the default) returns every row except the ones hidden in the app.
            `competitors` keeps the rows marked as competitors. `hidden` keeps only
            the hidden rows. Your own domains or brands are kept in every mode.
          enum:
          - all
          - competitors
          - hidden
          title: Filter
          type: string
      - description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
          Keeps your own rows and the rows carrying at least one of these tags, after
          `filter`. Distinct from `tag_ids`, which are prompt tags.
        in: query
        name: competitor_tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
            Keeps your own rows and the rows carrying at least one of these tags,
            after `filter`. Distinct from `tag_ids`, which are prompt tags.
          title: Competitor Tag Ids
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines: null
                data:
                - count: 62
                  count_delta: 4
                  is_competitor: true
                  is_hidden: false
                  is_user: false
                  last_check_count: 7
                  last_check_share: 35.0
                  last_check_share_delta: 5.0
                  name: competitor.com
                  rank: 1
                  rank_delta: 0
                  share: 31.0
                  share_delta: 1.5
                  tags:
                  - color: '#E5484D'
                    id: 3
                    name: Direct competitors
                  top3_share: 19.35
                  top3_share_delta: 2.1
                  total_rank: 1
                  trend:
                  - 30.0
                  - 35.0
                  trend_counts:
                  - 6
                  - 7
                - count: 28
                  count_delta: -2
                  is_competitor: false
                  is_hidden: false
                  is_user: false
                  last_check_count: 3
                  last_check_share: 15.0
                  last_check_share_delta: 0.0
                  name: rival.io
                  rank: 2
                  rank_delta: 0
                  share: 14.0
                  share_delta: -1.0
                  tags: []
                  top3_share: 7.14
                  top3_share_delta: null
                  total_rank: 2
                  trend:
                  - 15.0
                  - 15.0
                  trend_counts:
                  - 3
                  - 3
                hero:
                  avg_top_position: 4.1
                  avg_top_position_delta: -0.3
                  checks_total: 30
                  checks_with_user_position: 11
                  featured_snippet_rate: 0.12
                  featured_snippet_rate_delta: 0.02
                limit: 50
                offset: 0
                summary:
                  first_place_name: competitor.com
                  gap_to_first: 30.0
                  last_check_share: 5.0
                  last_check_share_delta: 0.0
                  position_insight: null
                  ranked_entity_count: 35
                  share: 6.5
                  share_delta: 0.5
                  total_count: 200
                  user_rank: 6
                  user_rank_delta: 1
                  user_ranks:
                  - last_check_share: 5.0
                    last_check_share_delta: 0.0
                    name: yourbrand.com
                    rank: 6
                    rank_delta: 1
                    share: 6.5
                    share_delta: 0.5
                    total_rank: 6
                total: 2
              schema:
                $ref: '#/components/schemas/SerpCompetitorsResponse'
          description: Paginated list of domains ranked by their appearance in the
            top-10 Bing organic SERP captured alongside Bing AI Overview checks, plus
            user-specific hero metrics (avg_top_position, featured_snippet_rate),
            the project's `summary` and the `applied` window. `total` counts the domains
            reachable by paging this endpoint after `filter` and `competitor_tag_ids`,
            so `ceil(total / limit)` is the page count. It is not a count of SERP
            appearances.
        '422':
          content:
            application/json:
              examples:
                competitor_tag_ids_cap:
                  summary: competitor_tag_ids holds more than 100 ids
                  value:
                    code: VALIDATION_ERROR
                    error: competitor_tag_ids takes at most 100 ids
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a malformed filter
      summary: List Bing-organic SERP competitors
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"limit\": 50},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/bing-organic/{domain}/prompts:
    get:
      description: 'Returns the prompts behind one row of `competitors/bing-organic`:
        every tracked prompt

        whose top-10 Bing organic results, captured alongside its

        Bing AI Overview checks, list the domain in the window, with how many results

        held it, its best position and when it last appeared. This is the list

        the app shows when a Bing SERP row is expanded.


        **`domain` is the ranking row''s `name`.** Lower-cased, and a leading `www.`
        is optional because both spellings are one row. Send a host name only: a scheme,
        port, path, `?`, `#` or whitespace answers 422.


        **The window and prompt filter are the ranking row''s.** An appearance

        counts when the Bing organic results captured with a check

        dated `date_from` through `date_to`, inclusive (UTC check day), list the

        domain in their top 10, for a prompt `tag_ids` keeps. So for the same

        filters the `rank_count` values, summed over every page, equal the

        `count` of the `competitors/bing-organic` row this drills from. The window
        defaults to

        the last 30 days and spans at most 365. These results have one engine

        of origin, so `engines` and `url_ids` are not taken and

        `applied.engines` is null.


        Every row is returned: `total` counts them all and `limit` / `offset`

        page through them. The default sort is `rank_count:desc`, ties keeping a

        stable order; `sort` takes `rank_count`, `best_position`, `last_ranked_at`
        or `text`. A `brand`, `domain` or `url` with no rows in

        the window answers 200 with empty `data` and `total: 0`.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `domain` is not a host name or is longer than
        253 characters; the `sort` field is not one of the allowed ones; `tag_ids`
        is malformed; `limit` or `offset` is out of range; a malformed date, `date_from`
        after `date_to` or after today |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: bing_organic_prompts_v1_projects__project_id__competitors_bing_organic__domain__prompts_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Domain, as `name` on the ranking row. Case-insensitive, and
          a leading `www.` is optional: both spellings are one row. A host name only,
          with no scheme, port or path; 1-253 characters.'
        in: path
        name: domain
        required: true
        schema:
          description: 'Domain, as `name` on the ranking row. Case-insensitive, and
            a leading `www.` is optional: both spellings are one row. A host name
            only, with no scheme, port or path; 1-253 characters.'
          maxLength: 253
          minLength: 1
          title: Domain
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines: null
                data:
                - best_position: 2
                  last_ranked_at: '2026-04-21T06:14:09+00:00'
                  prompt_id: 12377
                  rank_count: 12
                  text: best crm for startups
                - best_position: 4
                  last_ranked_at: '2026-04-18T06:02:51+00:00'
                  prompt_id: 12391
                  rank_count: 5
                  text: crm with email automation
                domain: competitor.com
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorSerpPromptListResponse'
          description: Paginated list of the prompts whose Bing top-10 organic results
            list this domain in the window, with each prompt's appearance count and
            best position, and the `applied` window. `total` counts the prompts reachable
            by paging.
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                domain_not_a_host:
                  summary: domain is not a host name
                  value:
                    code: VALIDATION_ERROR
                    error: domain must be a host name such as example.com, with no
                      scheme, port, path or whitespace
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a parameter was refused
      summary: List the prompts behind a Bing-organic SERP row
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic/competitor.com/prompts?date_from=2026-03-22&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic/competitor.com/prompts?date_from=2026-03-22&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic/competitor.com/prompts\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          },\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata =\
          \ res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/bing-organic/{domain}/urls:
    get:
      description: 'Returns the pages behind one row of `competitors/bing-organic`:
        every URL of the domain

        that appeared in the top-10 Bing organic results captured

        alongside Bing AI Overview checks in the window, with how many results held

        it, its mean position (1-10, one decimal) and when it last appeared.


        **`domain` is the ranking row''s `name`.** Lower-cased, and a leading `www.`
        is optional because both spellings are one row. Send a host name only: a scheme,
        port, path, `?`, `#` or whitespace answers 422.


        **The window and prompt filter are the ranking row''s.** An appearance

        counts when the Bing organic results captured with a check

        dated `date_from` through `date_to`, inclusive (UTC check day), list the

        domain in their top 10, for a prompt `tag_ids` keeps. So for the same

        filters the `rank_count` values, summed over every page, equal the

        `count` of the `competitors/bing-organic` row this drills from. The window
        defaults to

        the last 30 days and spans at most 365. These results have one engine

        of origin, so `engines` and `url_ids` are not taken and

        `applied.engines` is null.


        Every row is returned: `total` counts them all and `limit` / `offset`

        page through them. The default sort is `rank_count:desc`, ties keeping a

        stable order; `sort` takes `rank_count`, `avg_position`, `last_ranked_at`
        or `url`. A `brand`, `domain` or `url` with no rows in

        the window answers 200 with empty `data` and `total: 0`.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `domain` is not a host name or is longer than
        253 characters; the `sort` field is not one of the allowed ones; `tag_ids`
        is malformed; `limit` or `offset` is out of range; a malformed date, `date_from`
        after `date_to` or after today |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: bing_organic_urls_v1_projects__project_id__competitors_bing_organic__domain__urls_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Domain, as `name` on the ranking row. Case-insensitive, and
          a leading `www.` is optional: both spellings are one row. A host name only,
          with no scheme, port or path; 1-253 characters.'
        in: path
        name: domain
        required: true
        schema:
          description: 'Domain, as `name` on the ranking row. Case-insensitive, and
            a leading `www.` is optional: both spellings are one row. A host name
            only, with no scheme, port or path; 1-253 characters.'
          maxLength: 253
          minLength: 1
          title: Domain
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines: null
                data:
                - avg_position: 2.4
                  last_ranked_at: '2026-04-21T06:14:09+00:00'
                  rank_count: 12
                  url: https://competitor.com/
                - avg_position: 6.2
                  last_ranked_at: '2026-04-18T06:02:51+00:00'
                  rank_count: 5
                  url: https://competitor.com/pricing
                domain: competitor.com
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorSerpUrlListResponse'
          description: Paginated list of this domain's pages in Bing top-10 organic
            results in the window, with each URL's appearance count and mean position,
            and the `applied` window. `total` counts the URLs reachable by paging.
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                domain_not_a_host:
                  summary: domain is not a host name
                  value:
                    code: VALIDATION_ERROR
                    error: domain must be a host name such as example.com, with no
                      scheme, port, path or whitespace
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a parameter was refused
      summary: List the ranked URLs behind a Bing-organic SERP row
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic/competitor.com/urls?date_from=2026-03-22&date_to=2026-04-21&sort=avg_position:asc\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic/competitor.com/urls?date_from=2026-03-22&date_to=2026-04-21&sort=avg_position:asc\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/bing-organic/competitor.com/urls\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"sort\": \"avg_position:asc\"},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/citations:
    get:
      description: 'Returns competitor citations - external domains cited in AI-engine

        answers to your tracked prompts - aggregated across the requested

        window. Use this to see who else gets named when users search for your

        category, to find link-building targets, or to feed a competitive

        intelligence report. The rows, their flags and the `summary` are

        computed by the same code as the app''s Competitors page, so for the

        same window and filters they show the same numbers.


        **Domains are named without a leading `www.`.** A citation of

        `www.x.com` and one of `x.com` count toward the same `x.com` row, so a

        site cited under both spellings is one row with the citations summed.


        **Rows hidden in the app are left out.** The ranking applies the

        competitor marks set on the app''s Competitors page. `filter=all` (the

        default) returns every row except the hidden ones, `filter=competitors`

        keeps the rows marked as competitors, and `filter=hidden` keeps only

        the hidden rows. Your own domains are kept in every mode and carry

        `is_user: true`. Each row carries `is_competitor`, `is_hidden` and its

        competitor `tags`; `competitor_tag_ids` keeps your rows plus the rows

        carrying at least one of those tags, after `filter`. Both apply to

        `group_by=domain` only, because the category and tag groupings read no

        marks, as in the app. When the marks cannot be read the request fails

        with 503 rather than showing hidden rows.


        **`rank` is the app''s # column.** It is the position by the most

        recent check''s count over every ranked domain, hidden ones included,

        so hiding or filtering a row never renumbers the others and a filtered

        page can skip numbers. `total_rank` orders by the window''s `count`

        instead. The default sort is `last_check_share:desc`, the app''s own

        (with `group_by=tag` your rows stay first, as below);

        `sort=rank:asc` gives the # order. The `*_delta` fields, `trend` and

        `trend_counts` are declared on the row with their units.


        **`summary` says where you stand, over every row rather than the

        page.** `user_rank` is your best-ranked domain''s `rank`, out of

        `ranked_entity_count`; `share`, `last_check_share` and their deltas

        are that row''s own values. `gap_to_first` is measured to

        `first_place_name`, the leader (or the runner-up when you are first),

        read from the full ranking, so the leader can be a row hidden in the

        app, exactly as on the app''s summary tile. `total_count` is a citation

        volume, not a row count. `applied` echoes the window and the engines

        the numbers cover.


        **`shopping_count` splits a domain row by citation surface.** A domain

        aggregates surfaces by construction, so `google.com` holds ordinary

        cited pages and Google Shopping product panels in one row. That field

        says how many of the row''s `count` were panels; the rest were ordinary

        pages. A `google.com` row that is almost all panels is describing

        product listings rather than a site outranking you, and a panel carries

        Google''s own host rather than the merchant''s, so no domain here names

        the brand behind a panel.


        **`category` says what kind of site each domain is.** It is one of

        `ugc` (forums, Q&A, reviews, blogs and video, where users write the

        content), `news`, `wiki`, `official` (company, brand, product,

        documentation and government sites), `social` or `other`, which also

        holds every domain no classification covers.


        **`group_by=category` ranks the categories instead of the domains.**

        Each row is one category with citations, so there are at most six, and

        `name` and `category` both hold the category key. The counts sum every

        citation the domain ranking counts over the same window and filters,

        including domains too far down to appear in the domain ranking and

        domains hidden in the app, so the category totals reconcile with the

        domain view as a whole rather than with the rows it returns.

        `shopping_count`, `share` and the sort fields mean what they mean on a

        domain row. No row is yours, so `summary.user_rank` is null.


        **`group_by=tag` ranks competitor tags instead of domains.** Each row

        is one competitor tag with at least one domain member, `name` is the

        tag name, and the row carries `tag_id`, `color` and `member_count`.

        `count`, `last_check_count` and `share` hold the tag''s total over the

        same window, filters and share denominator as the domain ranking, so a

        tag''s `share` equals the sum of its members'' shares. A member with no

        citations counts as 0 and stays in `member_count`. `average_count`,

        `average_last_check_count` and `average_share` are those totals divided

        by `member_count`, rounded to 2 decimals, and `members` lists every

        member with its own count and share. A domain can carry several tags,

        so tags overlap and tag rows do not add up to the total; for the same

        reason `summary.total_count` is the window''s whole citation volume.

        Your own domains keep their own rows, never count inside a tag, and

        lead the ranking in its own order, as in the app''s Tags

        view: `sort` orders only the tag rows after them.

        `category` is `null` on tag rows. `aggregate=average` divides each tag

        row''s counts, shares, deltas and per-check series by `member_count`

        (2 decimals) and re-ranks every row, as the app''s Average toggle does,

        so `count`, `last_check_count` and `shopping_count` can be fractions;

        the `average_*` fields stay the totals divided by `member_count`.


        The window is exactly `date_from` through `date_to`, inclusive: a check

        counts when its cycle started (UTC) on one of those days, and never the

        day before `date_from`. A `date_to` later than today reads as today. `last_check_*`

        describe the last check on or before `date_to`.


        Filter by `tag_ids` and `url_ids` to scope to a slice of prompts. Engine

        keys in the response are normalised to public canonical names. See

        [Filtering and sorting](/content/guides/filtering-and-sorting.html) and

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `filter` is not `all`, `competitors` or `hidden`;
        `competitor_tag_ids` is malformed or holds more than 100 ids; `filter` other
        than `all`, or `competitor_tag_ids`, is sent with a `group_by` other than
        `domain`; `aggregate=average` is sent with a `group_by` other than `tag`;
        or the `sort` field is not one of the allowed ones |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: citations_v1_projects__project_id__competitors_citations_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Which rows to keep, as the app's Competitors page filter does.
          `all` (the default) returns every row except the ones hidden in the app.
          `competitors` keeps the rows marked as competitors. `hidden` keeps only
          the hidden rows. Your own domains or brands are kept in every mode.
        in: query
        name: filter
        required: false
        schema:
          default: all
          description: Which rows to keep, as the app's Competitors page filter does.
            `all` (the default) returns every row except the ones hidden in the app.
            `competitors` keeps the rows marked as competitors. `hidden` keeps only
            the hidden rows. Your own domains or brands are kept in every mode.
          enum:
          - all
          - competitors
          - hidden
          title: Filter
          type: string
      - description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
          Keeps your own rows and the rows carrying at least one of these tags, after
          `filter`. Distinct from `tag_ids`, which are prompt tags.
        in: query
        name: competitor_tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
            Keeps your own rows and the rows carrying at least one of these tags,
            after `filter`. Distinct from `tag_ids`, which are prompt tags.
          title: Competitor Tag Ids
      - description: 'Row grouping. `domain` ranks the cited domains. `category` ranks
          the six source categories (`ugc`, `news`, `wiki`, `official`, `social`,
          `other`) over the same citations, and each row''s `name` is a category key.
          `tag` ranks the project''s competitor tags over the same citations: each
          row sums a tag''s domain members, `name` is the tag name and the row carries
          `tag_id`, `color`, `member_count`, `members` and the `average_*` fields.
          `filter` and `competitor_tag_ids` apply to `domain` only.'
        in: query
        name: group_by
        required: false
        schema:
          default: domain
          description: 'Row grouping. `domain` ranks the cited domains. `category`
            ranks the six source categories (`ugc`, `news`, `wiki`, `official`, `social`,
            `other`) over the same citations, and each row''s `name` is a category
            key. `tag` ranks the project''s competitor tags over the same citations:
            each row sums a tag''s domain members, `name` is the tag name and the
            row carries `tag_id`, `color`, `member_count`, `members` and the `average_*`
            fields. `filter` and `competitor_tag_ids` apply to `domain` only.'
          enum:
          - domain
          - category
          - tag
          title: Group By
          type: string
      - description: 'With `group_by=tag`, how a tag row counts its members: `total`
          (the default) sums them; `average` divides each tag row''s counts, shares,
          deltas and per-check series by `member_count` (2 decimals) and re-ranks
          every row, as the app''s Average toggle does. `average_*` stay the totals
          divided by `member_count` either way. Any other grouping with `average`
          returns 422.'
        in: query
        name: aggregate
        required: false
        schema:
          default: total
          description: 'With `group_by=tag`, how a tag row counts its members: `total`
            (the default) sums them; `average` divides each tag row''s counts, shares,
            deltas and per-check series by `member_count` (2 decimals) and re-ranks
            every row, as the app''s Average toggle does. `average_*` stay the totals
            divided by `member_count` either way. Any other grouping with `average`
            returns 422.'
          enum:
          - total
          - average
          title: Aggregate
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                  - copilot
                data:
                - category: official
                  count: 38
                  count_delta: 4
                  is_competitor: true
                  is_hidden: false
                  is_user: false
                  last_check_count: 9
                  last_check_share: 28.13
                  last_check_share_delta: 2.5
                  name: competitor.com
                  rank: 1
                  rank_delta: 0
                  share: 27.14
                  share_delta: 1.25
                  shopping_count: 6
                  tags:
                  - color: '#E5484D'
                    id: 3
                    name: Direct competitors
                  total_rank: 1
                  trend:
                  - 25.63
                  - 28.13
                  trend_counts:
                  - 8
                  - 9
                - category: official
                  count: 24
                  count_delta: -2
                  is_competitor: false
                  is_hidden: false
                  is_user: true
                  last_check_count: 6
                  last_check_share: 18.75
                  last_check_share_delta: 0.63
                  name: yourbrand.com
                  rank: 3
                  rank_delta: 1
                  share: 17.14
                  share_delta: -0.71
                  shopping_count: 0
                  tags: []
                  total_rank: 3
                  trend:
                  - 18.13
                  - 18.75
                  trend_counts:
                  - 5
                  - 6
                limit: 50
                offset: 0
                summary:
                  first_place_name: competitor.com
                  gap_to_first: 9.4
                  last_check_share: 18.75
                  last_check_share_delta: 0.63
                  position_insight: null
                  ranked_entity_count: 41
                  share: 17.14
                  share_delta: -0.71
                  total_count: 140
                  user_rank: 3
                  user_rank_delta: 1
                  user_ranks:
                  - last_check_share: 18.75
                    last_check_share_delta: 0.63
                    name: yourbrand.com
                    rank: 3
                    rank_delta: 1
                    share: 17.14
                    share_delta: -0.71
                    total_rank: 3
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorListResponse'
          description: Paginated list of competitor citations across engines, with
            the project's `summary` and the `applied` window and engines. `total`
            counts the rows reachable by paging this endpoint after `filter` and `competitor_tag_ids`,
            so `ceil(total / limit)` is the page count. It is not a citation volume.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                aggregate_needs_tag:
                  summary: aggregate=average sent without group_by=tag
                  value:
                    code: VALIDATION_ERROR
                    error: aggregate=average applies to group_by=tag only
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                filter_needs_domain:
                  summary: filter or competitor_tag_ids sent with group_by=category
                    or tag
                  value:
                    code: VALIDATION_ERROR
                    error: filter and competitor_tag_ids apply to group_by=domain
                      only
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a filter the grouping does not take
      summary: List competitor citations
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations?date_from=2026-03-22&date_to=2026-04-21&filter=competitors&competitor_tag_ids=3&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations?date_from=2026-03-22&date_to=2026-04-21&filter=competitors&competitor_tag_ids=3&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations\"\
          ,\n    params={\n        \"date_from\": \"2026-03-22\",\n        \"date_to\"\
          : \"2026-04-21\",\n        \"filter\": \"competitors\",\n        \"competitor_tag_ids\"\
          : \"3\",\n        \"limit\": 50,\n    },\n    headers={\"Authorization\"\
          : \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/citations/{domain}/prompts:
    get:
      description: 'Returns the prompts behind one row of `competitors/citations`:
        every

        tracked prompt whose engine answers cite the domain in the window, with

        how many of those answers cite it and when the latest did. Use it to see

        which questions send AI engines to a competitor''s site, the list the app

        shows when a citations row is expanded.


        **`domain` is the ranking row''s `name`.** Lower-cased, and a leading `www.`
        is optional because both spellings are one row. Send a host name only: a scheme,
        port, path, `?`, `#` or whitespace answers 422.


        **The window, engines and prompt filter are the ranking row''s.** A

        citation counts when the check that produced it is dated `date_from`

        through `date_to`, inclusive (a check is dated by the UTC day its cycle

        started), on one of the engines in `engines`, for a prompt `tag_ids`

        and `url_ids` keep. So for the same filters the `citation_count` values,

        summed over every page, equal the `count` of the `competitors/citations` row
        this

        drills from. The window defaults to the last 30 days and spans at most

        365. `applied` echoes the window and the engines read; an engine you

        asked for that the plan lacks is not in it.


        Every row is returned: `total` counts them all and `limit` / `offset`

        page through them. The default sort is `citation_count:desc`, ties keeping
        a

        stable order; `sort` takes `citation_count`, `last_cited_at` or `text`. A
        `brand`, `domain` or `url` with no rows in

        the window answers 200 with empty `data` and `total: 0`.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `domain` is not a host name or is longer than
        253 characters; the `sort` field is not one of the allowed ones; `tag_ids`
        or `url_ids` is malformed; `limit` or `offset` is out of range; a malformed
        date, `date_from` after `date_to` or after today |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: citation_prompts_v1_projects__project_id__competitors_citations__domain__prompts_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Domain, as `name` on the ranking row. Case-insensitive, and
          a leading `www.` is optional: both spellings are one row. A host name only,
          with no scheme, port or path; 1-253 characters.'
        in: path
        name: domain
        required: true
        schema:
          description: 'Domain, as `name` on the ranking row. Case-insensitive, and
            a leading `www.` is optional: both spellings are one row. A host name
            only, with no scheme, port or path; 1-253 characters.'
          maxLength: 253
          minLength: 1
          title: Domain
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                data:
                - citation_count: 9
                  last_cited_at: '2026-04-21T06:14:09+00:00'
                  prompt_id: 12377
                  text: best crm for startups
                - citation_count: 4
                  last_cited_at: '2026-04-18T06:02:51+00:00'
                  prompt_id: 12391
                  text: crm with email automation
                domain: competitor.com
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorCitationPromptListResponse'
          description: Paginated list of the prompts whose engine answers cite this
            domain in the window, with each prompt's citation count and the `applied`
            window and engines. `total` counts the prompts reachable by paging.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                domain_not_a_host:
                  summary: domain is not a host name
                  value:
                    code: VALIDATION_ERROR
                    error: domain must be a host name such as example.com, with no
                      scheme, port, path or whitespace
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a parameter was refused
      summary: List the prompts behind a competitor citations row
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations/competitor.com/prompts?date_from=2026-03-22&date_to=2026-04-21&tag_ids=3,8\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations/competitor.com/prompts?date_from=2026-03-22&date_to=2026-04-21&tag_ids=3,8\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations/competitor.com/prompts\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"tag_ids\": \"3,8\"},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/citations/{domain}/urls:
    get:
      description: 'Returns the pages behind one row of `competitors/citations`: every
        URL

        of the domain that engine answers cited in the window, with how many

        answers cited it and when the latest did. Each `url` is written as

        `competitors/sources` names it, so it can be passed to

        `competitors/sources/prompts` for the prompts behind that page.


        **`domain` is the ranking row''s `name`.** Lower-cased, and a leading `www.`
        is optional because both spellings are one row. Send a host name only: a scheme,
        port, path, `?`, `#` or whitespace answers 422.


        **The window, engines and prompt filter are the ranking row''s.** A

        citation counts when the check that produced it is dated `date_from`

        through `date_to`, inclusive (a check is dated by the UTC day its cycle

        started), on one of the engines in `engines`, for a prompt `tag_ids`

        and `url_ids` keep. So for the same filters the `citation_count` values,

        summed over every page, equal the `count` of the `competitors/citations` row
        this

        drills from. The window defaults to the last 30 days and spans at most

        365. `applied` echoes the window and the engines read; an engine you

        asked for that the plan lacks is not in it.


        Every row is returned: `total` counts them all and `limit` / `offset`

        page through them. The default sort is `citation_count:desc`, ties keeping
        a

        stable order; `sort` takes `citation_count`, `last_cited_at` or `url`. A `brand`,
        `domain` or `url` with no rows in

        the window answers 200 with empty `data` and `total: 0`.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `domain` is not a host name or is longer than
        253 characters; the `sort` field is not one of the allowed ones; `tag_ids`
        or `url_ids` is malformed; `limit` or `offset` is out of range; a malformed
        date, `date_from` after `date_to` or after today |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: citation_urls_v1_projects__project_id__competitors_citations__domain__urls_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Domain, as `name` on the ranking row. Case-insensitive, and
          a leading `www.` is optional: both spellings are one row. A host name only,
          with no scheme, port or path; 1-253 characters.'
        in: path
        name: domain
        required: true
        schema:
          description: 'Domain, as `name` on the ranking row. Case-insensitive, and
            a leading `www.` is optional: both spellings are one row. A host name
            only, with no scheme, port or path; 1-253 characters.'
          maxLength: 253
          minLength: 1
          title: Domain
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                data:
                - citation_count: 9
                  last_cited_at: '2026-04-21T06:14:09+00:00'
                  url: https://competitor.com/pricing
                - citation_count: 4
                  last_cited_at: '2026-04-18T06:02:51+00:00'
                  url: https://competitor.com/blog/crm-guide
                domain: competitor.com
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorCitationUrlListResponse'
          description: Paginated list of this domain's pages cited by engine answers
            in the window, with each URL's citation count and the `applied` window
            and engines. `total` counts the URLs reachable by paging.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                domain_not_a_host:
                  summary: domain is not a host name
                  value:
                    code: VALIDATION_ERROR
                    error: domain must be a host name such as example.com, with no
                      scheme, port, path or whitespace
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a parameter was refused
      summary: List the cited URLs behind a competitor citations row
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations/competitor.com/urls?date_from=2026-03-22&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations/competitor.com/urls?date_from=2026-03-22&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/citations/competitor.com/urls\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          },\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata =\
          \ res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/mentions:
    get:
      description: 'Returns competitor brand mentions in AI-engine answers to your
        tracked

        prompts, aggregated across the requested window. This is the brand-name

        counterpart to the citations endpoint: instead of cited URLs, it

        surfaces brands referenced in answer text. The rows, their flags and

        the `summary` are computed by the same code as the app''s Competitors

        page.


        **`name` is a canonical brand name, not a raw one.** Spelling variants

        of one brand are merged before the counts are taken, so a brand written

        three ways in three answers is one row here rather than three competing

        ones. That means a `name` in this response need not be a string any

        engine actually wrote, and `count` can exceed the occurrences of that

        exact spelling. `competitors/trends?type=brand` merges identically, so

        the ranking and the chart agree. `competitors/citations` and

        `competitors/sources` do **not** merge: domains and URLs are never

        aliased. Read

        [`/v1/projects/{project_id}/brand-aliases`](#tag/competitors) to see the

        merges your workspace configured; a standard dictionary of well-known

        variants applies behind those and is not enumerable, so two names can

        be merged without appearing there. A workspace entry wins wherever both

        cover the same name.


        **Brands hidden in the app are left out**, and `filter`,

        `competitor_tag_ids`, the row flags, `rank` and `summary` work exactly

        as on `competitors/citations`, over brands: your own brands carry

        `is_user: true`, and a mark or tag set on a merged spelling applies to

        the canonical row. `filter` and `competitor_tag_ids` apply to

        `group_by=brand` only.


        **`shopping_count` is always 0 here.** The field is part of the shared

        competitor row shape and a brand mention has no citation surface to

        split, so the zero is structural rather than a measurement. Read it on

        `competitors/citations` and `competitors/sources`, where the rows are

        cited things.


        **`group_by=tag` ranks competitor tags instead of brands.** Each row is

        one competitor tag with at least one brand member, `name` is the tag

        name, and the row carries `tag_id`, `color` and `member_count`.

        `count`, `last_check_count` and `share` hold the tag''s total over the

        same window, filters and share denominator as the brand ranking, so a

        tag''s `share` equals the sum of its members'' shares. A member with no

        mentions counts as 0 and stays in `member_count`. `average_count`,

        `average_last_check_count` and `average_share` are those totals divided

        by `member_count`, rounded to 2 decimals, and `members` lists every

        member with its own count and share. A brand can carry several tags,

        so tags overlap and tag rows do not add up to the total. Your own

        brands keep their own rows, never count inside a tag, and lead the

        ranking in its own order, as in the app''s Tags view:

        `sort` orders only the tag rows after them.

        `aggregate=average` divides and re-ranks the tag rows as on

        `competitors/citations`.


        The window is exactly `date_from` through `date_to`, inclusive: a check

        counts when its cycle started (UTC) on one of those days, and never the

        day before `date_from`. A `date_to` later than today reads as today. `last_check_*`

        describe the last check on or before `date_to`.


        Filter by `tag_ids` and `url_ids` to scope to a slice of prompts. Engine

        keys in the response are normalised to public canonical names. See

        [Filtering and sorting](/content/guides/filtering-and-sorting.html) and

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `filter` is not `all`, `competitors` or `hidden`;
        `competitor_tag_ids` is malformed or holds more than 100 ids; `filter` other
        than `all`, or `competitor_tag_ids`, is sent with `group_by=tag`; `aggregate=average`
        is sent without `group_by=tag`; or the `sort` field is not one of the allowed
        ones |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: mentions_v1_projects__project_id__competitors_mentions_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Which rows to keep, as the app's Competitors page filter does.
          `all` (the default) returns every row except the ones hidden in the app.
          `competitors` keeps the rows marked as competitors. `hidden` keeps only
          the hidden rows. Your own domains or brands are kept in every mode.
        in: query
        name: filter
        required: false
        schema:
          default: all
          description: Which rows to keep, as the app's Competitors page filter does.
            `all` (the default) returns every row except the ones hidden in the app.
            `competitors` keeps the rows marked as competitors. `hidden` keeps only
            the hidden rows. Your own domains or brands are kept in every mode.
          enum:
          - all
          - competitors
          - hidden
          title: Filter
          type: string
      - description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
          Keeps your own rows and the rows carrying at least one of these tags, after
          `filter`. Distinct from `tag_ids`, which are prompt tags.
        in: query
        name: competitor_tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
            Keeps your own rows and the rows carrying at least one of these tags,
            after `filter`. Distinct from `tag_ids`, which are prompt tags.
          title: Competitor Tag Ids
      - description: 'Row grouping. `brand` ranks brands. `tag` ranks the project''s
          competitor tags over the same mentions: each row sums a tag''s brand members,
          `name` is the tag name and the row carries `tag_id`, `color`, `member_count`,
          `members` and the `average_*` fields. `filter` and `competitor_tag_ids`
          apply to `brand` only.'
        in: query
        name: group_by
        required: false
        schema:
          default: brand
          description: 'Row grouping. `brand` ranks brands. `tag` ranks the project''s
            competitor tags over the same mentions: each row sums a tag''s brand members,
            `name` is the tag name and the row carries `tag_id`, `color`, `member_count`,
            `members` and the `average_*` fields. `filter` and `competitor_tag_ids`
            apply to `brand` only.'
          enum:
          - brand
          - tag
          title: Group By
          type: string
      - description: 'With `group_by=tag`, how a tag row counts its members: `total`
          (the default) sums them; `average` divides each tag row''s counts, shares,
          deltas and per-check series by `member_count` (2 decimals) and re-ranks
          every row, as the app''s Average toggle does. `average_*` stay the totals
          divided by `member_count` either way. Any other grouping with `average`
          returns 422.'
        in: query
        name: aggregate
        required: false
        schema:
          default: total
          description: 'With `group_by=tag`, how a tag row counts its members: `total`
            (the default) sums them; `average` divides each tag row''s counts, shares,
            deltas and per-check series by `member_count` (2 decimals) and re-ranks
            every row, as the app''s Average toggle does. `average_*` stay the totals
            divided by `member_count` either way. Any other grouping with `average`
            returns 422.'
          enum:
          - total
          - average
          title: Aggregate
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                  - copilot
                data:
                - count: 47
                  count_delta: 5
                  is_competitor: true
                  is_hidden: false
                  is_user: false
                  last_check_count: 12
                  last_check_share: 33.33
                  last_check_share_delta: 1.4
                  name: RivalBrand
                  rank: 1
                  rank_delta: 0
                  share: 31.33
                  share_delta: 2.1
                  shopping_count: 0
                  tags:
                  - color: '#E5484D'
                    id: 3
                    name: Direct competitors
                  total_rank: 1
                  trend:
                  - 31.93
                  - 33.33
                  trend_counts:
                  - 11
                  - 12
                - count: 19
                  count_delta: -1
                  is_competitor: false
                  is_hidden: false
                  is_user: true
                  last_check_count: 5
                  last_check_share: 13.89
                  last_check_share_delta: 0.3
                  name: AcmeCorp
                  rank: 2
                  rank_delta: 0
                  share: 12.67
                  share_delta: -0.6
                  shopping_count: 0
                  tags: []
                  total_rank: 2
                  trend:
                  - 13.59
                  - 13.89
                  trend_counts:
                  - 5
                  - 5
                limit: 50
                offset: 0
                summary:
                  first_place_name: RivalBrand
                  gap_to_first: 19.4
                  last_check_share: 13.89
                  last_check_share_delta: 0.3
                  position_insight: null
                  ranked_entity_count: 28
                  share: 12.67
                  share_delta: -0.6
                  total_count: 150
                  user_rank: 2
                  user_rank_delta: 0
                  user_ranks:
                  - last_check_share: 13.89
                    last_check_share_delta: 0.3
                    name: AcmeCorp
                    rank: 2
                    rank_delta: 0
                    share: 12.67
                    share_delta: -0.6
                    total_rank: 2
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorListResponse'
          description: Paginated list of competitor mentions across engines, with
            the project's `summary` and the `applied` window and engines. `total`
            counts the rows reachable by paging this endpoint after `filter` and `competitor_tag_ids`,
            so `ceil(total / limit)` is the page count. It is not a mention volume.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                aggregate_needs_tag:
                  summary: aggregate=average sent without group_by=tag
                  value:
                    code: VALIDATION_ERROR
                    error: aggregate=average applies to group_by=tag only
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                filter_needs_brand:
                  summary: filter or competitor_tag_ids sent with group_by=tag
                  value:
                    code: VALIDATION_ERROR
                    error: filter and competitor_tag_ids apply to group_by=brand only
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a filter the grouping does not take
      summary: List competitor mentions
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/mentions?date_from=2026-03-22&date_to=2026-04-21&filter=competitors&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/mentions?date_from=2026-03-22&date_to=2026-04-21&filter=competitors&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/mentions\"\
          ,\n    params={\n        \"date_from\": \"2026-03-22\",\n        \"date_to\"\
          : \"2026-04-21\",\n        \"filter\": \"competitors\",\n        \"limit\"\
          : 50,\n    },\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n\
          )\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/mentions/{brand}/prompts:
    get:
      description: 'Returns the prompts behind one row of `competitors/mentions`:
        every

        tracked prompt whose engine answers mention the brand in the window,

        with how many of those answers mention it and when the latest did. Use

        it to see which questions surface a competitor, the same list the app

        shows when a mentions row is expanded.


        **`brand` is resolved through the same merge map as

        `competitors/mentions`**, so a `name` taken from that ranking counts

        every spelling folded into it. `brand` in the response echoes what you

        sent and `canonical_brand` names the merged brand. Percent-encode the

        name as one path segment; a `/` in it is sent as `%2F` and is

        supported here.


        **The window, engines and prompt filter are the ranking row''s.** A

        mention counts when the check that produced it is dated `date_from`

        through `date_to`, inclusive (a check is dated by the UTC day its cycle

        started), on one of the engines in `engines`, for a prompt `tag_ids`

        and `url_ids` keep. So for the same filters the `mention_count` values,

        summed over every page, equal the `count` of the `competitors/mentions` row
        this

        drills from. The window defaults to the last 30 days and spans at most

        365. `applied` echoes the window and the engines read; an engine you

        asked for that the plan lacks is not in it.


        Every row is returned: `total` counts them all and `limit` / `offset`

        page through them. The default sort is `mention_count:desc`, ties keeping
        a

        stable order; `sort` takes `mention_count`, `last_mentioned_at` or `text`.
        A `brand`, `domain` or `url` with no rows in

        the window answers 200 with empty `data` and `total: 0`.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `brand` is empty or longer than 200 characters;
        the `sort` field is not one of the allowed ones; `tag_ids` or `url_ids` is
        malformed; `limit` or `offset` is out of range; a malformed date, `date_from`
        after `date_to` or after today |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: mention_prompts_v1_projects__project_id__competitors_mentions__brand__prompts_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Brand name, as `name` on a `competitors/mentions` row. Case-insensitive,
          and resolved through the same merge map as that ranking. Percent-encode
          it; a name containing `/` is supported as `%2F`. 1-200 characters.
        in: path
        name: brand
        required: true
        schema:
          description: Brand name, as `name` on a `competitors/mentions` row. Case-insensitive,
            and resolved through the same merge map as that ranking. Percent-encode
            it; a name containing `/` is supported as `%2F`. 1-200 characters.
          maxLength: 200
          minLength: 1
          title: Brand
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                brand: RivalBrand
                canonical_brand: RivalBrand
                data:
                - last_mentioned_at: '2026-04-21T06:14:09+00:00'
                  mention_count: 9
                  prompt_id: 12377
                  text: best crm for startups
                - last_mentioned_at: '2026-04-18T06:02:51+00:00'
                  mention_count: 4
                  prompt_id: 12391
                  text: crm with email automation
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorMentionPromptListResponse'
          description: Paginated list of the prompts whose engine answers mention
            this brand in the window, with each prompt's mention count, the brand
            echo and the `applied` window and engines. `total` counts the prompts
            reachable by paging, so `ceil(total / limit)` is the page count.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a parameter was refused
      summary: List the prompts behind a competitor mentions row
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/mentions/RivalBrand/prompts?date_from=2026-03-22&date_to=2026-04-21&engines=chatgpt,perplexity\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/mentions/RivalBrand/prompts?date_from=2026-03-22&date_to=2026-04-21&engines=chatgpt,perplexity\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/mentions/RivalBrand/prompts\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"engines\": \"chatgpt,perplexity\"},\n    headers={\"Authorization\"\
          : \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/serp:
    get:
      description: 'Returns competitor domains ranked by their appearance in the top-10

        Google organic SERP captured alongside Google AI Overviews (AIO)

        checks. Each appearance represents one (check, position) pair where

        the domain held a top-10 slot in the organic results panel. Use this

        to compare traditional Google SEO standings against the AIO citation

        landscape on the same set of tracked queries.


        Returns a paginated list of domains plus a top-level `hero` block with

        user-specific metrics: `avg_top_position` (mean of the user''s best

        organic rank across checks where they appeared) and

        `featured_snippet_rate` (fraction of AIO checks whose featured snippet

        URL belongs to a user domain), each with a delta vs the previous

        half-window.


        **Domains hidden in the app are left out**, and `filter`,

        `competitor_tag_ids`, the row flags, `rank` and `summary` work exactly

        as on `competitors/citations`, over the same domain marks. The default

        sort is `last_check_share:desc`; `top3_share` and `top3_share_delta`

        sort too.


        The window is exactly `date_from` through `date_to`, inclusive: a check

        counts when its cycle started (UTC) on one of those days, and never the

        day before `date_from`. A `date_to` later than today reads as today. `last_check_*`

        describe the last check on or before `date_to`, and the `hero` deltas

        compare the two halves of that window.


        SERP data is captured exclusively on AIO checks, so the endpoint

        takes no `engines` filter and `applied.engines` is null. It takes no

        `url_ids` or `q` either; any of the three sent anyway is dropped like

        any other parameter the operation does not declare. Filter by

        `tag_ids` to scope to a slice of prompts.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `filter` is not `all`, `competitors` or `hidden`;
        `competitor_tag_ids` is malformed or holds more than 100 ids; or the `sort`
        field is not one of the allowed ones |


        See [error codes](/content/error-codes.html) for the standard errors

        (401, 402, 403, 422, 429, 500, 503).

        '
      operationId: serp_v1_projects__project_id__competitors_serp_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Which rows to keep, as the app's Competitors page filter does.
          `all` (the default) returns every row except the ones hidden in the app.
          `competitors` keeps the rows marked as competitors. `hidden` keeps only
          the hidden rows. Your own domains or brands are kept in every mode.
        in: query
        name: filter
        required: false
        schema:
          default: all
          description: Which rows to keep, as the app's Competitors page filter does.
            `all` (the default) returns every row except the ones hidden in the app.
            `competitors` keeps the rows marked as competitors. `hidden` keeps only
            the hidden rows. Your own domains or brands are kept in every mode.
          enum:
          - all
          - competitors
          - hidden
          title: Filter
          type: string
      - description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
          Keeps your own rows and the rows carrying at least one of these tags, after
          `filter`. Distinct from `tag_ids`, which are prompt tags.
        in: query
        name: competitor_tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
            Keeps your own rows and the rows carrying at least one of these tags,
            after `filter`. Distinct from `tag_ids`, which are prompt tags.
          title: Competitor Tag Ids
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines: null
                data:
                - count: 142
                  count_delta: 6
                  is_competitor: false
                  is_hidden: false
                  is_user: false
                  last_check_count: 19
                  last_check_share: 19.0
                  last_check_share_delta: 0.5
                  name: wikipedia.org
                  rank: 1
                  rank_delta: 0
                  share: 18.2
                  share_delta: 0.4
                  tags: []
                  top3_share: 64.0
                  top3_share_delta: 1.5
                  total_rank: 1
                  trend:
                  - 18.5
                  - 19.0
                  trend_counts:
                  - 18
                  - 19
                - count: 80
                  count_delta: 2
                  is_competitor: true
                  is_hidden: false
                  is_user: false
                  last_check_count: 8
                  last_check_share: 8.0
                  last_check_share_delta: -1.0
                  name: rival.com
                  rank: 2
                  rank_delta: 1
                  share: 10.3
                  share_delta: 0.2
                  tags:
                  - color: '#E5484D'
                    id: 3
                    name: Direct competitors
                  top3_share: 25.0
                  top3_share_delta: -4.0
                  total_rank: 2
                  trend:
                  - 9.0
                  - 8.0
                  trend_counts:
                  - 9
                  - 8
                - count: 71
                  count_delta: -3
                  is_competitor: false
                  is_hidden: false
                  is_user: true
                  last_check_count: 7
                  last_check_share: 7.0
                  last_check_share_delta: -1.0
                  name: yourbrand.com
                  rank: 3
                  rank_delta: -1
                  share: 9.1
                  share_delta: -0.4
                  tags: []
                  top3_share: 18.0
                  top3_share_delta: null
                  total_rank: 3
                  trend:
                  - 8.0
                  - 7.0
                  trend_counts:
                  - 8
                  - 7
                hero:
                  avg_top_position: 3.4
                  avg_top_position_delta: -0.5
                  checks_total: 30
                  checks_with_user_position: 14
                  featured_snippet_rate: 0.18
                  featured_snippet_rate_delta: 0.03
                limit: 50
                offset: 0
                summary:
                  first_place_name: wikipedia.org
                  gap_to_first: 12.0
                  last_check_share: 7.0
                  last_check_share_delta: -1.0
                  position_insight: null
                  ranked_entity_count: 64
                  share: 9.1
                  share_delta: -0.4
                  total_count: 780
                  user_rank: 3
                  user_rank_delta: -1
                  user_ranks:
                  - last_check_share: 7.0
                    last_check_share_delta: -1.0
                    name: yourbrand.com
                    rank: 3
                    rank_delta: -1
                    share: 9.1
                    share_delta: -0.4
                    total_rank: 3
                total: 3
              schema:
                $ref: '#/components/schemas/SerpCompetitorsResponse'
          description: Paginated list of domains ranked by their appearance in the
            top-10 Google organic SERP captured alongside AI Overview checks, plus
            user-specific hero metrics (avg_top_position, featured_snippet_rate),
            the project's `summary` and the `applied` window. `total` counts the domains
            reachable by paging this endpoint after `filter` and `competitor_tag_ids`,
            so `ceil(total / limit)` is the page count. It is not a count of SERP
            appearances.
        '422':
          content:
            application/json:
              examples:
                competitor_tag_ids_cap:
                  summary: competitor_tag_ids holds more than 100 ids
                  value:
                    code: VALIDATION_ERROR
                    error: competitor_tag_ids takes at most 100 ids
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a malformed filter
      summary: List Google-organic SERP competitors
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"limit\": 50},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/serp/{domain}/prompts:
    get:
      description: 'Returns the prompts behind one row of `competitors/serp`: every
        tracked prompt

        whose top-10 Google organic results, captured alongside its

        Google AI Overview checks, list the domain in the window, with how many results

        held it, its best position and when it last appeared. This is the list

        the app shows when a Google SERP row is expanded.


        **`domain` is the ranking row''s `name`.** Lower-cased, and a leading `www.`
        is optional because both spellings are one row. Send a host name only: a scheme,
        port, path, `?`, `#` or whitespace answers 422.


        **The window and prompt filter are the ranking row''s.** An appearance

        counts when the Google organic results captured with a check

        dated `date_from` through `date_to`, inclusive (UTC check day), list the

        domain in their top 10, for a prompt `tag_ids` keeps. So for the same

        filters the `rank_count` values, summed over every page, equal the

        `count` of the `competitors/serp` row this drills from. The window defaults
        to

        the last 30 days and spans at most 365. These results have one engine

        of origin, so `engines` and `url_ids` are not taken and

        `applied.engines` is null.


        Every row is returned: `total` counts them all and `limit` / `offset`

        page through them. The default sort is `rank_count:desc`, ties keeping a

        stable order; `sort` takes `rank_count`, `best_position`, `last_ranked_at`
        or `text`. A `brand`, `domain` or `url` with no rows in

        the window answers 200 with empty `data` and `total: 0`.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `domain` is not a host name or is longer than
        253 characters; the `sort` field is not one of the allowed ones; `tag_ids`
        is malformed; `limit` or `offset` is out of range; a malformed date, `date_from`
        after `date_to` or after today |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: serp_prompts_v1_projects__project_id__competitors_serp__domain__prompts_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Domain, as `name` on the ranking row. Case-insensitive, and
          a leading `www.` is optional: both spellings are one row. A host name only,
          with no scheme, port or path; 1-253 characters.'
        in: path
        name: domain
        required: true
        schema:
          description: 'Domain, as `name` on the ranking row. Case-insensitive, and
            a leading `www.` is optional: both spellings are one row. A host name
            only, with no scheme, port or path; 1-253 characters.'
          maxLength: 253
          minLength: 1
          title: Domain
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines: null
                data:
                - best_position: 2
                  last_ranked_at: '2026-04-21T06:14:09+00:00'
                  prompt_id: 12377
                  rank_count: 12
                  text: best crm for startups
                - best_position: 4
                  last_ranked_at: '2026-04-18T06:02:51+00:00'
                  prompt_id: 12391
                  rank_count: 5
                  text: crm with email automation
                domain: competitor.com
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorSerpPromptListResponse'
          description: Paginated list of the prompts whose Google top-10 organic results
            list this domain in the window, with each prompt's appearance count and
            best position, and the `applied` window. `total` counts the prompts reachable
            by paging.
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                domain_not_a_host:
                  summary: domain is not a host name
                  value:
                    code: VALIDATION_ERROR
                    error: domain must be a host name such as example.com, with no
                      scheme, port, path or whitespace
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a parameter was refused
      summary: List the prompts behind a Google-organic SERP row
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp/competitor.com/prompts?date_from=2026-03-22&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp/competitor.com/prompts?date_from=2026-03-22&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp/competitor.com/prompts\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          },\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata =\
          \ res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/serp/{domain}/urls:
    get:
      description: 'Returns the pages behind one row of `competitors/serp`: every
        URL of the domain

        that appeared in the top-10 Google organic results captured

        alongside Google AI Overview checks in the window, with how many results held

        it, its mean position (1-10, one decimal) and when it last appeared.


        **`domain` is the ranking row''s `name`.** Lower-cased, and a leading `www.`
        is optional because both spellings are one row. Send a host name only: a scheme,
        port, path, `?`, `#` or whitespace answers 422.


        **The window and prompt filter are the ranking row''s.** An appearance

        counts when the Google organic results captured with a check

        dated `date_from` through `date_to`, inclusive (UTC check day), list the

        domain in their top 10, for a prompt `tag_ids` keeps. So for the same

        filters the `rank_count` values, summed over every page, equal the

        `count` of the `competitors/serp` row this drills from. The window defaults
        to

        the last 30 days and spans at most 365. These results have one engine

        of origin, so `engines` and `url_ids` are not taken and

        `applied.engines` is null.


        Every row is returned: `total` counts them all and `limit` / `offset`

        page through them. The default sort is `rank_count:desc`, ties keeping a

        stable order; `sort` takes `rank_count`, `avg_position`, `last_ranked_at`
        or `url`. A `brand`, `domain` or `url` with no rows in

        the window answers 200 with empty `data` and `total: 0`.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `domain` is not a host name or is longer than
        253 characters; the `sort` field is not one of the allowed ones; `tag_ids`
        is malformed; `limit` or `offset` is out of range; a malformed date, `date_from`
        after `date_to` or after today |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: serp_urls_v1_projects__project_id__competitors_serp__domain__urls_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Domain, as `name` on the ranking row. Case-insensitive, and
          a leading `www.` is optional: both spellings are one row. A host name only,
          with no scheme, port or path; 1-253 characters.'
        in: path
        name: domain
        required: true
        schema:
          description: 'Domain, as `name` on the ranking row. Case-insensitive, and
            a leading `www.` is optional: both spellings are one row. A host name
            only, with no scheme, port or path; 1-253 characters.'
          maxLength: 253
          minLength: 1
          title: Domain
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines: null
                data:
                - avg_position: 2.4
                  last_ranked_at: '2026-04-21T06:14:09+00:00'
                  rank_count: 12
                  url: https://competitor.com/
                - avg_position: 6.2
                  last_ranked_at: '2026-04-18T06:02:51+00:00'
                  rank_count: 5
                  url: https://competitor.com/pricing
                domain: competitor.com
                limit: 50
                offset: 0
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorSerpUrlListResponse'
          description: Paginated list of this domain's pages in Google top-10 organic
            results in the window, with each URL's appearance count and mean position,
            and the `applied` window. `total` counts the URLs reachable by paging.
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                domain_not_a_host:
                  summary: domain is not a host name
                  value:
                    code: VALIDATION_ERROR
                    error: domain must be a host name such as example.com, with no
                      scheme, port, path or whitespace
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a parameter was refused
      summary: List the ranked URLs behind a Google-organic SERP row
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp/competitor.com/urls?date_from=2026-03-22&date_to=2026-04-21&sort=avg_position:asc\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp/competitor.com/urls?date_from=2026-03-22&date_to=2026-04-21&sort=avg_position:asc\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/serp/competitor.com/urls\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"sort\": \"avg_position:asc\"},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/sources:
    get:
      description: 'Returns the individual source URLs cited in AI-engine answers
        to your

        tracked prompts, aggregated across the requested window. This is the

        URL-level counterpart to the citations endpoint: `citations` ranks the

        domains that get cited, this ranks the specific pages, so it is the one

        that answers which article or documentation page an engine actually

        drew from.


        `name` carries the full URL rather than a host or a brand.


        **Competitor marks apply by the URL''s host.** A URL on a domain hidden

        in the app is left out, `is_user` is true on URLs on your own domains,

        and `tags` are the competitor tags of the URL''s host. `filter`,

        `competitor_tag_ids`, `rank` and `summary` otherwise work exactly as on

        `competitors/citations`. This endpoint has no grouping.


        **`shopping_count` saturates here.** A URL is one kind of thing, so a

        Google Shopping product panel''s row has `shopping_count == count` and an

        ordinary page''s row has 0; nothing in between is reachable on this

        endpoint. Treat it as the boolean `competitors/citations` cannot give

        you, and read the panel URL''s `productid` rather than its host, which is

        always Google''s. The same field on `competitors/citations` is a partial

        split, because a domain aggregates both surfaces.


        The window is exactly `date_from` through `date_to`, inclusive: a check

        counts when its cycle started (UTC) on one of those days, and never the

        day before `date_from`. A `date_to` later than today reads as today. `last_check_*`

        describe the last check on or before `date_to`.


        Filter by `tag_ids` and `url_ids` to scope to a slice of prompts.

        Engine keys in the response are normalised to public canonical names.

        See

        [Filtering and sorting](/content/guides/filtering-and-sorting.html) and

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `filter` is not `all`, `competitors` or `hidden`;
        `competitor_tag_ids` is malformed or holds more than 100 ids; or the `sort`
        field is not one of the allowed ones |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: sources_v1_projects__project_id__competitors_sources_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Which rows to keep, as the app's Competitors page filter does.
          `all` (the default) returns every row except the ones hidden in the app.
          `competitors` keeps the rows marked as competitors. `hidden` keeps only
          the hidden rows. Your own domains or brands are kept in every mode.
        in: query
        name: filter
        required: false
        schema:
          default: all
          description: Which rows to keep, as the app's Competitors page filter does.
            `all` (the default) returns every row except the ones hidden in the app.
            `competitors` keeps the rows marked as competitors. `hidden` keeps only
            the hidden rows. Your own domains or brands are kept in every mode.
          enum:
          - all
          - competitors
          - hidden
          title: Filter
          type: string
      - description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
          Keeps your own rows and the rows carrying at least one of these tags, after
          `filter`. Distinct from `tag_ids`, which are prompt tags.
        in: query
        name: competitor_tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated competitor tag ids, at most 100, each 1-2147483647.
            Keeps your own rows and the rows carrying at least one of these tags,
            after `filter`. Distinct from `tag_ids`, which are prompt tags.
          title: Competitor Tag Ids
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                data:
                - count: 21
                  count_delta: 3
                  is_competitor: true
                  is_hidden: false
                  is_user: false
                  last_check_count: 6
                  last_check_share: 35.29
                  last_check_share_delta: 0.9
                  name: https://competitor.com/blog/best-crm-tools
                  rank: 1
                  rank_delta: 0
                  share: 33.87
                  share_delta: 1.94
                  shopping_count: 0
                  tags:
                  - color: '#E5484D'
                    id: 3
                    name: Direct competitors
                  total_rank: 1
                  trend:
                  - 34.39
                  - 35.29
                  trend_counts:
                  - 5
                  - 6
                - count: 13
                  count_delta: 0
                  is_competitor: false
                  is_hidden: false
                  is_user: false
                  last_check_count: 3
                  last_check_share: 17.65
                  last_check_share_delta: -2.1
                  name: https://www.google.com/search?q=best+crm&prds=pvt:hg,productid:1234567890&ibp=oshop
                  rank: 2
                  rank_delta: -1
                  share: 20.97
                  share_delta: -0.3
                  shopping_count: 13
                  tags: []
                  total_rank: 2
                  trend:
                  - 19.75
                  - 17.65
                  trend_counts:
                  - 4
                  - 3
                limit: 50
                offset: 0
                summary:
                  first_place_name: https://competitor.com/blog/best-crm-tools
                  gap_to_first: null
                  last_check_share: null
                  last_check_share_delta: null
                  position_insight: null
                  ranked_entity_count: 57
                  share: null
                  share_delta: null
                  total_count: 62
                  user_rank: null
                  user_rank_delta: null
                  user_ranks: []
                total: 2
              schema:
                $ref: '#/components/schemas/CompetitorListResponse'
          description: Paginated list of the individual source URLs cited in AI-engine
            answers to this project's tracked prompts, with the project's `summary`
            and the `applied` window and engines. `total` counts the URLs reachable
            by paging this endpoint after `filter` and `competitor_tag_ids`, so `ceil(total
            / limit)` is the page count. It is not a citation volume.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                competitor_tag_ids_cap:
                  summary: competitor_tag_ids holds more than 100 ids
                  value:
                    code: VALIDATION_ERROR
                    error: competitor_tag_ids takes at most 100 ids
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a malformed filter
      summary: List competitor sources
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/sources?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/sources?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/sources\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"limit\": 50},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/sources/prompts:
    get:
      description: 'Returns the prompts behind one row of `competitors/sources`: every

        tracked prompt whose engine answers cite the page in the window, with

        how many of those answers cite it and when the latest did, the list the

        app shows when a Top Sources row is expanded.


        **`url` is the row''s `name`, matched exactly.** Pass it as a query

        parameter, percent-encoded, starting with `http://` or `https://`. A URL

        `competitors/sources` does not list answers with no rows.


        **The window, engines and prompt filter are the ranking row''s.** A

        citation counts when the check that produced it is dated `date_from`

        through `date_to`, inclusive (a check is dated by the UTC day its cycle

        started), on one of the engines in `engines`, for a prompt `tag_ids`

        and `url_ids` keep. So for the same filters the `citation_count` values,

        summed over every page, equal the `count` of the `competitors/sources` row
        this

        drills from. The window defaults to the last 30 days and spans at most

        365. `applied` echoes the window and the engines read; an engine you

        asked for that the plan lacks is not in it.


        Every row is returned: `total` counts them all and `limit` / `offset`

        page through them. The default sort is `citation_count:desc`, ties keeping
        a

        stable order; `sort` takes `citation_count`, `last_cited_at` or `text`. A
        `brand`, `domain` or `url` with no rows in

        the window answers 200 with empty `data` and `total: 0`.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `url` is missing, does not start with `http://`
        or `https://`, or is longer than 2048 characters; the `sort` field is not
        one of the allowed ones; `tag_ids` or `url_ids` is malformed; `limit` or `offset`
        is out of range; a malformed date, `date_from` after `date_to` or after today
        |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: source_prompts_v1_projects__project_id__competitors_sources_prompts_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: The source URL, exactly as `name` on a `competitors/sources`
          row. Must start with `http://` or `https://`; 1-2048 characters.
        in: query
        name: url
        required: true
        schema:
          description: The source URL, exactly as `name` on a `competitors/sources`
            row. Must start with `http://` or `https://`; 1-2048 characters.
          maxLength: 2048
          minLength: 1
          title: Url
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                data:
                - citation_count: 9
                  last_cited_at: '2026-04-21T06:14:09+00:00'
                  prompt_id: 12377
                  text: best crm for startups
                - citation_count: 4
                  last_cited_at: '2026-04-18T06:02:51+00:00'
                  prompt_id: 12391
                  text: crm with email automation
                limit: 50
                offset: 0
                total: 2
                url: https://competitor.com/pricing
              schema:
                $ref: '#/components/schemas/CompetitorSourcePromptListResponse'
          description: Paginated list of the prompts whose engine answers cite this
            URL in the window, with each prompt's citation count and the `applied`
            window and engines. `total` counts the prompts reachable by paging.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                url_not_http:
                  summary: url is not an http(s) URL
                  value:
                    code: VALIDATION_ERROR
                    error: url must start with http:// or https://
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a parameter was refused
      summary: List the prompts behind a competitor sources row
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/sources/prompts?url=https%3A%2F%2Fcompetitor.com%2Fpricing&date_from=2026-03-22&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/sources/prompts?url=https%3A%2F%2Fcompetitor.com%2Fpricing&date_from=2026-03-22&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/sources/prompts\"\
          ,\n    params={\"url\": \"https://competitor.com/pricing\", \"date_from\"\
          : \"2026-03-22\", \"date_to\": \"2026-04-21\"},\n    headers={\"Authorization\"\
          : \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/trends:
    get:
      description: 'Returns a time-series of competitor share over the requested window,

        one series per competitor on a shared check-date axis. `type` picks

        what the series follow: `domain` (the default) for citation share,

        `brand` for mention share, and `serp` and `bing_organic` for top-10

        organic share on Google and Bing. Use this to plot how your

        share-of-voice moves against named competitors over time.


        **`group_by=category` plots source categories instead of domains.**

        With `type=domain` each series is one source category with citations

        in the window (`ugc`, `news`, `wiki`, `official`, `social`, `other`),

        and its `name` is the category key. The shares are taken over the same

        citations the domain series divide, so at each check the category

        series sum to 100 up to rounding. `is_user` is `false` on every

        category series. `group_by=category` with any other `type` returns 422.


        **`group_by=tag` plots competitor tags instead of entities.** With

        `type=domain` or `type=brand`, each series is one competitor tag with

        members of that type, plus your own series. A tag series carries

        `tag_id`, `color` and `member_count`; its `data` and `counts` hold the

        tag total, and `average_data` is `data` divided by `member_count`,

        rounded to 2 decimals with nulls kept. `aggregate=average` divides each

        tag series'' `data` and `counts` by `member_count` (2 decimals), as the

        app''s Average toggle does; `average_data` stays the totals divided by

        `member_count` either way. `group_by=tag` with `type=serp` or

        `type=bing_organic` returns 422, and so does `aggregate=average`

        without `group_by=tag`.


        With `type=domain` a series names its domain without a leading `www.`,

        so `www.x.com` and `x.com` are one `x.com` series.


        The window is exactly `date_from` through `date_to`, inclusive: a check

        counts when its cycle started (UTC) on one of those days, and never the

        day before `date_from`. A `date_to` later than today reads as today.

        `applied` echoes the window and the engines read; with `type=serp` or

        `type=bing_organic`, whose data has one engine of origin,

        `applied.engines` is null.


        Filter by `tag_ids` to scope to a slice of prompts. Engine keys in the

        response are normalised to public canonical names. See

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `type` is not one of the four above, `group_by`
        is not `domain`, `category` or `tag`, `group_by=category` is sent with a `type`
        other than `domain`, `group_by=tag` is sent with a `type` other than `domain`
        or `brand`, or `aggregate=average` is sent without `group_by=tag` |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: trends_v1_projects__project_id__competitors_trends_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Competitor type to retrieve trends for: domain, brand, serp
          (Google-organic top-10 share) or bing_organic (Bing-organic top-10 share).
          `serp` and `bing_organic` are the trend counterparts of the `competitors/serp`
          and `competitors/bing-organic` rankings. For `brand`, series are keyed by
          canonical brand name - spelling variants of one brand are merged before
          shares are computed, matching the mentions ranking.'
        in: query
        name: type
        required: false
        schema:
          default: domain
          description: 'Competitor type to retrieve trends for: domain, brand, serp
            (Google-organic top-10 share) or bing_organic (Bing-organic top-10 share).
            `serp` and `bing_organic` are the trend counterparts of the `competitors/serp`
            and `competitors/bing-organic` rankings. For `brand`, series are keyed
            by canonical brand name - spelling variants of one brand are merged before
            shares are computed, matching the mentions ranking.'
          title: Type
          type: string
      - description: Series grouping. `domain` plots one series per cited domain (or
          brand, for `type=brand`). `category` plots one series per source category
          (`ugc`, `news`, `wiki`, `official`, `social`, `other`), named by its key;
          `group_by=category` with any `type` other than `domain` returns 422. `tag`
          plots one series per competitor tag with members of the requested `type`
          (`domain` or `brand`), plus your own entities' series. `group_by=tag` with
          `serp` or `bing_organic` returns 422.
        in: query
        name: group_by
        required: false
        schema:
          default: domain
          description: Series grouping. `domain` plots one series per cited domain
            (or brand, for `type=brand`). `category` plots one series per source category
            (`ugc`, `news`, `wiki`, `official`, `social`, `other`), named by its key;
            `group_by=category` with any `type` other than `domain` returns 422. `tag`
            plots one series per competitor tag with members of the requested `type`
            (`domain` or `brand`), plus your own entities' series. `group_by=tag`
            with `serp` or `bing_organic` returns 422.
          enum:
          - domain
          - category
          - tag
          title: Group By
          type: string
      - description: With `group_by=tag`, `average` divides each tag series' `data`
          and `counts` by `member_count` (2 decimals), as the app's Average toggle
          does; `total` (the default) keeps the sums. `average_data` stays the totals
          divided by `member_count` either way. Any other grouping with `average`
          returns 422.
        in: query
        name: aggregate
        required: false
        schema:
          default: total
          description: With `group_by=tag`, `average` divides each tag series' `data`
            and `counts` by `member_count` (2 decimals), as the app's Average toggle
            does; `total` (the default) keeps the sums. `average_data` stays the totals
            divided by `member_count` either way. Any other grouping with `average`
            returns 422.
          enum:
          - total
          - average
          title: Aggregate
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                checks:
                - '2026-04-13'
                - '2026-04-20'
                series:
                - counts:
                  - 5
                  - 7
                  data:
                  - 31.25
                  - 35.0
                  denominator:
                  - 16
                  - 20
                  is_user: false
                  name: competitor.com
                - counts:
                  - 3
                  - 4
                  data:
                  - 18.75
                  - 20.0
                  denominator:
                  - 16
                  - 20
                  is_user: true
                  name: yourbrand.com
                total_per_check:
                - 16
                - 20
              schema:
                $ref: '#/components/schemas/CompetitorTrendsResponse'
          description: Time-series of competitor share over the requested window,
            with the `applied` window and engines.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                aggregate_needs_tag:
                  summary: aggregate=average sent without group_by=tag
                  value:
                    code: VALIDATION_ERROR
                    error: aggregate=average applies to group_by=tag only
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                group_by_needs_domain:
                  summary: group_by=category sent with a type other than domain
                  value:
                    code: VALIDATION_ERROR
                    error: group_by=category applies to type=domain only
                group_by_tag_needs_domain_or_brand:
                  summary: group_by=tag sent with serp or bing_organic
                  value:
                    code: VALIDATION_ERROR
                    error: group_by=tag applies to type=domain or type=brand only
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or a grouping the type does not take
      summary: Get competitor trends
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/trends?type=domain&date_from=2026-03-22&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/trends?type=domain&date_from=2026-03-22&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/trends\"\
          ,\n    params={\"type\": \"domain\", \"date_from\": \"2026-03-22\", \"date_to\"\
          : \"2026-04-21\"},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/competitors/{brand}/details:
    get:
      description: 'Returns aggregated discrete facts mentioned about a single named
        brand

        across the AI-engine answers tracked for this project, within the

        requested window. Use this to drill into what AI engines actually say

        about a competitor: their pricing, features, claims, or sentiment, with

        occurrence counts and the most recent check that produced each fact.


        **The window is the mentions ranking''s.** A fact counts when the check

        that produced it is dated `date_from` through `date_to`, inclusive (a

        check is dated by the UTC day its cycle started), and ran on one of

        the engines in `engines`, so the facts follow the

        `competitors/mentions` row they drill from. The window defaults to

        the last 30 days and spans at most 365. `applied` echoes the window

        and the engines read. The deprecated `days` reads the trailing

        `[today - days, today]` and cannot be combined with either date.


        Filter by `tag_ids` to scope the aggregation to a slice of prompts.

        `limit` (default 100, max 100) bounds how many distinct facts are

        returned.


        **`brand` is resolved through the same merge map as

        `competitors/mentions`, and every spelling folded into it is

        aggregated.** A brand written `Acme Inc.` in one answer and `Acme` in

        another is one lookup here whenever a merge covers the two, exactly as

        on `competitors/mentions` and `competitors/trends?type=brand`. So a

        `name` taken straight from the mentions ranking returns the facts

        behind that row''s `count`, including facts recorded under a spelling no

        engine ever wrote against that name. Percent-encode the name as one

        path segment; a brand name containing `/` is not supported here.


        Send any spelling. `brand` in the response echoes what you sent;

        `canonical_brand` names the merged entity the facts describe, so two

        requests that come back with the same `canonical_brand` describe the

        same set of facts and must not be added together. The merges your

        workspace configured are listed by

        [`/v1/projects/{project_id}/brand-aliases`](#tag/competitors), though

        the `variants` there are normalised fingerprints rather than the

        original casing, and the standard dictionary that also merges names is

        not enumerable; `canonical_brand` is how you tell that a merge applied

        without being able to enumerate it. Where no merge covers the name, the

        lookup is the case-insensitive match it has always been and

        `canonical_brand` echoes your spelling.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | `days` is sent with `date_from` or `date_to`,
        `url_ids` is sent, or `tag_ids` is malformed |


        See [error codes](/content/error-codes.html) for the standard errors

        (401, 402, 403, 422, 429, 500, 503).

        '
      operationId: get_competitor_details_v1_projects__project_id__competitors__brand__details_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Brand name to look up. Case-insensitive, and resolved through
          the same merge map as `competitors/mentions`, so a canonical name from that
          ranking reaches every spelling folded into it. Percent-encode it as one
          path segment; a name containing `/` is not supported.
        in: path
        name: brand
        required: true
        schema:
          description: Brand name to look up. Case-insensitive, and resolved through
            the same merge map as `competitors/mentions`, so a canonical name from
            that ranking reaches every spelling folded into it. Percent-encode it
            as one path segment; a name containing `/` is not supported.
          title: Brand
          type: string
      - deprecated: true
        description: 'Deprecated: use date_from/date_to. Trailing [today-days, today],
          1 to 365. Cannot be combined with `date_from` or `date_to`.'
        in: query
        name: days
        required: false
        schema:
          anyOf:
          - maximum: 365
            minimum: 1
            type: integer
          - type: 'null'
          deprecated: true
          description: 'Deprecated: use date_from/date_to. Trailing [today-days, today],
            1 to 365. Cannot be combined with `date_from` or `date_to`.'
          title: Days
      - description: Max number of facts returned, 1-100. Defaults to 100.
        in: query
        name: limit
        required: false
        schema:
          default: 100
          description: Max number of facts returned, 1-100. Defaults to 100.
          maximum: 100
          minimum: 1
          title: Limit
          type: integer
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                brand: RivalBrand
                canonical_brand: RivalBrand
                days: 30
                facts:
                - detail: Offers a 14-day free trial on the Pro plan.
                  last_seen_check_no: 4821
                  occurrence_count: 18
                  sentiment: positive
                - detail: Pricing starts at $29/month per seat.
                  last_seen_check_no: 4815
                  occurrence_count: 11
                  sentiment: neutral
                - detail: Customers cite slow support response times.
                  last_seen_check_no: 4799
                  occurrence_count: 4
                  sentiment: negative
                total_distinct_facts: 12
              schema:
                $ref: '#/components/schemas/CompetitorDetailsRespV1'
          description: Aggregated discrete facts mentioned about this brand in the
            project's tracked answers, from the checks dated inside the window, with
            the `applied` window and engines.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                days_with_dates:
                  summary: The deprecated days sent with date_from or date_to
                  value:
                    code: VALIDATION_ERROR
                    error: days cannot be combined with date_from or date_to
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window too wide, or days combined with a date
      summary: Get competitor brand facts
      tags:
      - Competitors
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/competitors/RivalBrand/details?date_from=2026-03-22&date_to=2026-04-21&limit=20\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/competitors/RivalBrand/details?date_from=2026-03-22&date_to=2026-04-21&limit=20\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/competitors/RivalBrand/details\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"limit\": 20},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/dashboard/llm-trends:
    get:
      description: 'Returns engine-by-engine trend lines for the project over the
        requested

        window - one series per AI engine showing daily share or visibility.

        Use this to plot how your presence across ChatGPT, Google AI Overviews,

        Perplexity, Google AI Mode, and Microsoft Copilot evolves over time.


        **The response is parallel arrays, not a list of points.** `checks` is

        the date axis and every array under an engine is index-aligned with it,

        so a value''s date comes from `checks[i]` rather than from the value

        itself. A null rate is a bucket with no answered check for that engine:

        either no check ran, or every check failed, in which case

        `failed_count[i]` says how many. Failed checks are excluded from every

        rate and `*_denominator`. A `null` is not the same as a zero. Rates are

        percentages on a 0-100 scale, and the `*_count` / `*_denominator`

        arrays carry the integers behind them so you can re-aggregate over a

        longer period instead of averaging averages.

        Only engines with a bucket in the window appear as keys.


        The window is exactly `date_from` through `date_to`, inclusive, and a

        check is dated by the UTC day its cycle started. The `checks` axis lists

        every day of the window, `date_to` included, whether the window ends

        today or earlier; a day with no check carries `null` rates and zero

        counts. A `date_to` later than today reads as today. `applied` echoes

        the window and the engines the series cover, after defaults and

        clamping.


        `engine_summary` is the app''s best and worst engine strip: each

        engine''s score on the most recent day in the window on which it

        answered, and its move from the first such day, in points. It equals

        the overview''s `engine_summary` for the same window, `engines` and

        `tag_ids`.


        Filter by `tag_ids` to scope to a slice of prompts. Engine keys in the

        response are normalised to public canonical names. See

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: llm_trends_v1_projects__project_id__dashboard_llm_trends_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-04-13'
                  date_to: '2026-04-15'
                  engines:
                  - chatgpt
                  - google_aio
                checks:
                - '2026-04-13'
                - '2026-04-14'
                - '2026-04-15'
                engine_summary:
                  chatgpt:
                    failed_checks: 1
                    first_check: '2026-04-13'
                    last_check: '2026-04-15'
                    newer_failed_at: null
                    newer_failed_checks: 0
                    success_score: 69.0
                    success_score_delta: 11.0
                  google_aio:
                    failed_checks: 0
                    first_check: '2026-04-15'
                    last_check: '2026-04-15'
                    newer_failed_at: null
                    newer_failed_checks: 0
                    success_score: 19.0
                    success_score_delta: null
                engines:
                  chatgpt:
                    citations_count:
                    - 3
                    - 3
                    - 4
                    citations_denominator:
                    - 8
                    - 8
                    - 8
                    citations_rate:
                    - 38.0
                    - 44.0
                    - 50.0
                    failed_count:
                    - 0
                    - 0
                    - 1
                    mentions_count:
                    - 5
                    - 5
                    - 6
                    mentions_denominator:
                    - 8
                    - 8
                    - 8
                    mentions_rate:
                    - 62.0
                    - 69.0
                    - 75.0
                    sentiment_denominator:
                    - 4
                    - 5
                    - 6
                    sentiment_mixed_count:
                    - 1
                    - 1
                    - 1
                    sentiment_negative_count:
                    - 0
                    - 0
                    - 0
                    sentiment_positive_count:
                    - 3
                    - 4
                    - 5
                    sentiment_score:
                    - 75.0
                    - 80.0
                    - 83.0
                    success_count:
                    - 6
                    - 7
                    - 9
                    success_denominator:
                    - 16
                    - 16
                    - 16
                    success_score:
                    - 58.0
                    - 63.0
                    - 69.0
                  google_aio:
                    citations_count:
                    - 0
                    - 0
                    - 1
                    citations_denominator:
                    - 0
                    - 0
                    - 8
                    citations_rate:
                    - null
                    - null
                    - 13.0
                    failed_count:
                    - 0
                    - 8
                    - 0
                    mentions_count:
                    - 0
                    - 0
                    - 2
                    mentions_denominator:
                    - 0
                    - 0
                    - 8
                    mentions_rate:
                    - null
                    - null
                    - 25.0
                    sentiment_denominator:
                    - 0
                    - 0
                    - 0
                    sentiment_mixed_count:
                    - 0
                    - 0
                    - 0
                    sentiment_negative_count:
                    - 0
                    - 0
                    - 0
                    sentiment_positive_count:
                    - 0
                    - 0
                    - 0
                    sentiment_score:
                    - null
                    - null
                    - null
                    success_count:
                    - 0
                    - 0
                    - 1
                    success_denominator:
                    - 0
                    - 0
                    - 16
                    success_score:
                    - null
                    - null
                    - 19.0
              schema:
                $ref: '#/components/schemas/DashboardLlmTrendsResponse'
          description: Engine-by-engine trend lines over the requested window.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window validation failed
      summary: Get LLM trends
      tags:
      - Dashboard
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/dashboard/llm-trends?date_from=2026-03-22&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/dashboard/llm-trends?date_from=2026-03-22&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/dashboard/llm-trends\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          },\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata =\
          \ res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/dashboard/overview:
    get:
      description: "Returns the top-line metrics for a project over the requested\
        \ window,\ncomputed by the same code as the ziptie.ai Overview page: the KPI\n\
        cards (`kpis`), each engine's latest score (`engine_summary`), the\nmost-cited\
        \ domains and most-mentioned brands (`top_competitors`), the\ndaily success-score\
        \ trend and the same score broken down by tag. Use\nthis as the single call\
        \ to populate a project home screen or to drive\na daily summary report.\n\
        \nThe window is exactly `date_from` through `date_to`, inclusive, and a\n\
        check is dated by the UTC day its cycle started. Every `trends` series\nlists\
        \ every day of the window, `date_to` included; a day with no\nanswered check\
        \ has a `null` score. A `date_to` later than today reads\nas today. `applied`\
        \ echoes the window and the engines the numbers\ncover, after defaults and\
        \ clamping.\n\n**Rates are percentages on a 0-100 scale**, with one decimal\
        \ place,\nin every block: `kpis`, `engine_summary`, `trends`,\n`tag_success_score`\
        \ and `top_competitors`. The `*_raw`, `*_count`\nand `*_denominator` fields\
        \ are the integers behind them.\n\n`kpis` are the app's KPI cards. Each rate\
        \ is read from\n`kpis.last_check`, the most recent day in the window on which\
        \ a check\nanswered, and each delta is that day minus `kpis.first_check`,\
        \ the\nfirst such day, in points. `engine_summary` applies the same rule to\n\
        each engine's own series, keyed by public engine name. Without\n`url_ids`\
        \ it equals `engine_summary` on `/dashboard/llm-trends` for the\nsame window,\
        \ `engines` and `tag_ids`; with `url_ids` it covers only the\nprompts linked\
        \ to those URLs, which `/dashboard/llm-trends` cannot\nnarrow to.\n\n`top_competitors`\
        \ is the app's Competitive Landscape: the ten\nmost-cited domains and the\
        \ ten most-mentioned brands, ordered by\ntheir count in the window, with the\
        \ rows you hid in the app left out\nand `is_user` set on your own. A row's\
        \ `count` and `percentage` are\nthe `count` and `share` that `/competitors/citations`\
        \ and\n`/competitors/mentions` return for the same window and filters, and\n\
        `user_domain_ranks` / `user_brand_ranks` place your domains and\nbrands in\
        \ the full rankings.\n\nFour of the blocks - `trends`, `top_citations`, `top_brands`\
        \ and\n`tag_success_score` - are keyed by public engine name plus `combined`\n\
        for the same figure across every enabled engine. An engine the project\nhas\
        \ not enabled is present with an empty array rather than absent, so\nyou can\
        \ iterate the keys without existence checks. A block is `null`\nrather than\
        \ empty when the window holds no checks at all; that is the\ndifference between\
        \ \"nothing was measured\" and \"nothing was found\".\n`kpis` and `top_competitors`\
        \ are `null` exactly when `trends` is. See\n[Engine names](/content/guides/engine-names.html).\n\
        \nOn a `combined` entry the rates are the mean of the answering\nengines'\
        \ rates, while the counts are sums over those engines, so a\ncombined count\
        \ divided by a combined denominator is not the combined\nrate. To pool engines\
        \ yourself, sum each engine's counts.\n\n`top_competitors` and `top_citations`\
        \ rank whole domains, named\nwithout a leading `www.` (`www.x.com` and `x.com`\
        \ are one `x.com`\nentry, counts summed). A domain says nothing about the\
        \ kind of page\nbehind it, so read a `google.com` row carefully: a Google\
        \ Shopping\nproduct panel carries Google's own host rather than the merchant's,\n\
        and is counted under it. Panels are included here by design, and no\nparameter\
        \ on this endpoint removes them. To see which rows are\npanels, read the `source_kind`\
        \ on a check's `sources` or on\n`/dashboard/ugc-impact/{platform}`, which\
        \ is also the only endpoint\nthat filters on it.\n\n**Failed checks are left\
        \ out of every score.** A failed check is an\nengine run that returned no\
        \ answer. It is not counted in any rate or\ndenominator; each `trends` point\
        \ and `tag_success_score` entry reports\nit beside the score as `failed_checks`,\
        \ and `kpis` and\n`engine_summary` report the failures on and after their\
        \ scored day. A\ndate, or a tag on an engine, whose checks all failed has\
        \ a `null`\nscore with its `failed_checks`, never a 0.\n\n`total_count`: without\
        \ `tag_ids` or `url_ids`, prompts with at least\none check that did not fail\
        \ in the window. With either filter: the\nprompts the filter matches. This\
        \ counts prompts, not checks.\n\n`insight` is prose a language model wrote\
        \ over the project's own\nnumbers, about the days in `insight.date_span`,\
        \ which can differ from\nthe request's window. Its shape is stable; its wording\
        \ is not, and it\nis `null` until the out-of-band pipeline has written one,\
        \ which is\nnormal for a project whose first check batch has not finished.\
        \ Treat\nit as copy to display, never as a field to parse.\n\n**Deprecated\
        \ fields**, still served:\n\n- `per_engine`: use `engine_summary`; see below.\n\
        - `top_citations` and `top_brands`: use `top_competitors`. They are the\n\
        \  upstream rankings per engine, with the rows you hid in the app\n  included\
        \ and no flag for your own.\n- `insight.nudges`: raw suggestions the writer\
        \ generated, not filtered\n  by what you accepted or dismissed. The app shows\
        \ its Action Center\n  instead.\n\n`per_engine` is the one block that does\
        \ **not** describe the window: it\ncarries the score from the most recent\
        \ check batch in which that engine\nanswered, its move against the batch before\
        \ it, and `failed_checks`,\n`newer_failed_checks` and `newer_failed_at`, which\
        \ report failed checks\nin and after that batch. `newer_failed_checks` is\
        \ 0 when nothing newer\nfailed, and `null` only where those batches predate\
        \ failed-check counts.\nAn engine that has answered in no batch appears only\
        \ when its checks in\nthe newest check batch failed: its `success_score` is\
        \ `null`, its\n`failed_checks` counts those failures and its `newer_failed_checks`\
        \ is\n`null`. The block does not shift with `date_from` and `date_to`, and\
        \ it\nignores `engines`, `tag_ids` and `url_ids`.\n\nFilter by `tag_ids` and\
        \ `url_ids` to scope to a slice of prompts.\nInternal flags such as `has_data`,\
        \ `user_domains` and `user_brands` are\nstripped from the response.\n\nThe\
        \ `thresholds` block carries the project's own good/fair display\nbands for\
        \ these metrics. They band the numbers for presentation and\nare not inputs\
        \ to any of them, so a project that has moved its bands\nstill reports the\
        \ same `success_score`. The score is measured on the\nproject's own prompts,\
        \ engines, country and cadence, so compare it with\nthe project's own past,\
        \ never with another project's.\n\n**Errors specific to this endpoint:**\n\
        \n| Code | HTTP | When |\n|------|------|------|\n| `INVALID_ENGINE_NAME`\
        \ | 400 | `engines` names an engine that does not exist |\n| `ENGINE_NOT_ENABLED`\
        \ | 400 | Every engine in `engines` is one the workspace's plan does not include\
        \ |\n| `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days\
        \ |\n\nSee [error codes](/content/error-codes.html) for the standard errors\
        \ (401, 402, 403, 422, 429, 500, 503).\n"
      operationId: overview_v1_projects__project_id__dashboard_overview_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                engine_summary:
                  chatgpt:
                    failed_checks: 1
                    first_check: '2026-04-20'
                    last_check: '2026-04-21'
                    newer_failed_at: null
                    newer_failed_checks: 0
                    success_score: 69.0
                    success_score_delta: 11.0
                  google_aio:
                    failed_checks: 0
                    first_check: '2026-04-21'
                    last_check: '2026-04-21'
                    newer_failed_at: null
                    newer_failed_checks: 0
                    success_score: 19.0
                    success_score_delta: null
                insight:
                  body: Your citation rate on ChatGPT moved from 38% to 50% across
                    the two most recent batches, driven by pricing prompts. Google
                    AI Overviews recorded its first mentions this window but has not
                    cited you yet.
                  date_span:
                    end: '2026-04-21'
                    start: '2026-04-20'
                  generated_at: '2026-04-21T06:12:44Z'
                  headline: Citations rose on ChatGPT while AI Overviews stayed flat.
                  key_highlight: positive
                  nudges:
                  - cited_batch_dates:
                    - '2026-04-21'
                    severity: warning
                    text: Add prompts covering integrations - RivalBrand leads there
                      and you are absent.
                  period:
                    failed_checks: 9
                    total_checks: 32
                kpis:
                  citations_count: 5
                  citations_rate: 32.0
                  citations_rate_delta: -6.0
                  citations_total: 16
                  failed_checks: 1
                  failed_checks_in_range: 9
                  failed_only_checks: []
                  failed_only_days: []
                  first_check: '2026-04-20'
                  last_check: '2026-04-21'
                  mentions_count: 8
                  mentions_rate: 50.0
                  mentions_rate_delta: -12.0
                  mentions_total: 16
                  newer_failed_at: null
                  newer_failed_checks: 0
                  sentiment_mixed: 1
                  sentiment_negative: 0
                  sentiment_positive: 5
                  sentiment_score: 83.0
                  sentiment_score_delta: 8.0
                  success_score: 44.0
                  success_score_delta: -14.0
                per_engine:
                  chatgpt:
                    failed_checks: 1
                    newer_failed_at: null
                    newer_failed_checks: 0
                    success_score: 69.0
                    success_score_delta: 11.0
                  google_aio:
                    failed_checks: 0
                    newer_failed_at: null
                    newer_failed_checks: 0
                    success_score: 19.0
                    success_score_delta: null
                tag_success_score:
                  bing_ai_overview: []
                  chatgpt:
                  - color: '#1EBE73'
                    failed_checks: 0
                    id_tag: 41
                    name: pricing
                    success_score:
                      brand_appeared_count_raw: 3
                      brand_mentions: 75.0
                      citations: 50.0
                      citations_count_raw: 2
                      citations_denominator: 4
                      denominator: 4
                      mentions_count_raw: 3
                      sentiment: 83.0
                      sentiment_mixed: 1
                      sentiment_negative: 0
                      sentiment_positive: 5
                      success_score: 69.0
                  combined:
                  - color: '#1EBE73'
                    failed_checks: 0
                    id_tag: 41
                    name: pricing
                    success_score:
                      brand_appeared_count_raw: 3
                      brand_mentions: 75.0
                      citations: 50.0
                      citations_count_raw: 2
                      citations_denominator: 4
                      denominator: 4
                      mentions_count_raw: 3
                      sentiment: 83.0
                      sentiment_mixed: 1
                      sentiment_negative: 0
                      sentiment_positive: 5
                      success_score: 69.0
                  copilot: []
                  gemini: []
                  google_ai_mode: []
                  google_aio: []
                  perplexity: []
                thresholds:
                  citations_target_fair: 15
                  citations_target_good: 30
                  mentions_target_fair: 15
                  mentions_target_good: 30
                  score_target_fair: 40
                  score_target_good: 70
                  sentiment_target_fair: 40
                  sentiment_target_good: 70
                top_brands:
                  bing_ai_overview: []
                  chatgpt:
                  - brand: RivalBrand
                    color: '#1BADFF'
                    count: 22
                  - brand: AcmeCorp
                    color: '#9574F1'
                    count: 9
                  combined:
                  - brand: RivalBrand
                    color: '#1BADFF'
                    count: 26
                  - brand: AcmeCorp
                    color: '#9574F1'
                    count: 9
                  copilot: []
                  gemini: []
                  google_ai_mode: []
                  google_aio:
                  - brand: RivalBrand
                    color: '#1BADFF'
                    count: 4
                  perplexity: []
                top_citations:
                  bing_ai_overview: []
                  chatgpt:
                  - color: '#1BADFF'
                    count: 27
                    domain: competitor.com
                  - color: '#9574F1'
                    count: 14
                    domain: rival.io
                  combined:
                  - color: '#1BADFF'
                    count: 33
                    domain: competitor.com
                  - color: '#9574F1'
                    count: 14
                    domain: rival.io
                  copilot: []
                  gemini: []
                  google_ai_mode: []
                  google_aio:
                  - color: '#1BADFF'
                    count: 6
                    domain: competitor.com
                  perplexity: []
                top_competitors:
                  brands:
                  - color: '#1BADFF'
                    count: 26
                    is_user: false
                    name: RivalBrand
                    percentage: 57.8
                    rank: 1
                  - color: '#9574F1'
                    count: 9
                    is_user: true
                    name: AcmeCorp
                    percentage: 20.0
                    rank: 2
                  domains:
                  - color: '#1BADFF'
                    count: 33
                    is_user: false
                    name: competitor.com
                    percentage: 41.3
                    rank: 1
                  - color: '#9574F1'
                    count: 25
                    is_user: true
                    name: acme.com
                    percentage: 31.3
                    rank: 2
                  - color: '#1EBE73'
                    count: 14
                    is_user: false
                    name: rival.io
                    percentage: 17.5
                    rank: 3
                  total_brands: 17
                  total_domains: 48
                  user_brand_ranks:
                  - name: AcmeCorp
                    rank: 2
                    total: 17
                  user_domain_ranks:
                  - name: acme.com
                    rank: 2
                    total: 48
                total_count: 8
                trends:
                  bing_ai_overview: []
                  chatgpt:
                  - date: '2026-04-20'
                    failed_checks: 0
                    success_score:
                      brand_appeared_count_raw: 6
                      brand_mentions: 62.0
                      citations: 38.0
                      citations_count_raw: 3
                      citations_denominator: 8
                      denominator: 8
                      mentions_count_raw: 5
                      sentiment: 75.0
                      sentiment_mixed: 1
                      sentiment_negative: 0
                      sentiment_positive: 3
                      success_score: 58.0
                  - date: '2026-04-21'
                    failed_checks: 1
                    success_score:
                      brand_appeared_count_raw: 7
                      brand_mentions: 75.0
                      citations: 50.0
                      citations_count_raw: 4
                      citations_denominator: 8
                      denominator: 8
                      mentions_count_raw: 6
                      sentiment: 83.0
                      sentiment_mixed: 1
                      sentiment_negative: 0
                      sentiment_positive: 5
                      success_score: 69.0
                  combined:
                  - date: '2026-04-20'
                    failed_checks: 8
                    success_score:
                      brand_appeared_count_raw: 6
                      brand_mentions: 62.0
                      citations: 38.0
                      citations_count_raw: 3
                      citations_denominator: 8
                      denominator: 8
                      mentions_count_raw: 5
                      sentiment: 75.0
                      sentiment_mixed: 1
                      sentiment_negative: 0
                      sentiment_positive: 3
                      success_score: 58.0
                  - date: '2026-04-21'
                    failed_checks: 1
                    success_score:
                      brand_appeared_count_raw: 9
                      brand_mentions: 50.0
                      citations: 32.0
                      citations_count_raw: 5
                      citations_denominator: 16
                      denominator: 16
                      mentions_count_raw: 8
                      sentiment: 83.0
                      sentiment_mixed: 1
                      sentiment_negative: 0
                      sentiment_positive: 5
                      success_score: 44.0
                  copilot: []
                  gemini: []
                  google_ai_mode: []
                  google_aio:
                  - date: '2026-04-20'
                    failed_checks: 8
                    success_score: null
                  - date: '2026-04-21'
                    failed_checks: 0
                    success_score:
                      brand_appeared_count_raw: 2
                      brand_mentions: 25.0
                      citations: 13.0
                      citations_count_raw: 1
                      citations_denominator: 8
                      denominator: 8
                      mentions_count_raw: 2
                      sentiment: null
                      sentiment_mixed: 0
                      sentiment_negative: 0
                      sentiment_positive: 0
                      success_score: 19.0
                  perplexity: []
                uncertainty:
                  changes:
                    citations_rate:
                      caveats:
                      - retries_pending
                      prompts: 4
                      reason: null
                      verdict: insufficient_sample
                    mentions_rate:
                      caveats: []
                      prompts: 4
                      reason: null
                      verdict: insufficient_sample
                    sentiment_score:
                      caveats: []
                      prompts: 3
                      reason: null
                      verdict: insufficient_sample
                    success_score: null
                  engines: {}
                  failure_heavy_share: 0.25
                  first_date: '2026-04-20'
                  kpis:
                    citations_rate:
                      high: null
                      low: null
                      prompts: 4
                      reason: low_sample
                      sample: 16
                      sample_unit: answers_with_links
                    mentions_rate:
                      high: null
                      low: null
                      prompts: 4
                      reason: low_sample
                      sample: 16
                      sample_unit: answers
                    sentiment_score:
                      high: null
                      low: null
                      prompts: 3
                      reason: low_sample
                      sample: 6
                      sample_unit: answers_with_sentiment
                    success_score: null
                  latest_date: '2026-04-21'
                  level: 0.95
                  method: prompt_cluster_linearized
                  method_version: 2
                  min_answers: 30
                  min_engine_sentiment_answers: 10
                  min_prompts: 20
                  settles_at: '2026-04-22T06:00:00Z'
              schema:
                $ref: '#/components/schemas/DashboardOverviewResponse'
          description: Top-line metrics for the project.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window validation failed
      summary: Get dashboard overview
      tags:
      - Dashboard
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/dashboard/overview?date_from=2026-03-22&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/dashboard/overview?date_from=2026-03-22&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/dashboard/overview\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          },\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata =\
          \ res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/dashboard/ugc-attributes:
    get:
      description: 'Returns every attribute detected on the UGC sources AI engines
        cited

        for your tracked prompts, grouped so each attribute appears once with

        the sources it was found on. An attribute is a short phrase the page

        analysis recorded about a product or brand, such as `pricing` or

        `ease of use`. Read `/dashboard/ugc-impact` to rank the platforms and

        `/dashboard/ugc-impact/{platform}` to read the sources of one; this

        reads across all of them.


        **Grouping.** Attributes are grouped by meaning: phrases that say the

        same thing in different words, such as `pricing` and `cost`, form one

        group. `label` is one member wording as written, chosen as the group''s

        display text. Each source lists the distinct wordings it carries in

        `wordings`, in the order the source lists them; `text` and `quotes` on

        the source repeat the first one.


        **Counting.** `source_count` is the number of distinct sources the

        attribute appears on: a URL counts once however many checks, prompts or

        engines cited it, and URLs differing only by a trailing `#:~:text=`

        scroll-to-text fragment are one source. `platform_count` is the number

        of distinct platforms among them and `brand_mentioned_count` how many

        of the sources name one of your tracked brands. `analyzed_source_count`

        is the number of sources in scope that carry at least one attribute,

        the denominator for `source_count`.


        **Order.** Groups come most widespread first (`source_count`

        descending, then `label` alphabetically). The order is fixed, so `sort`

        is not a parameter and is rejected with 422 `VALIDATION_ERROR` when

        sent. `total` counts groups after `q` and before paging. Each group

        lists its `sources`, newest check first and then by URL, capped at 100;

        `sources_truncated` is true when more carry the attribute.

        `general_sentiment` on a source is null wherever `is_brand_mentioned`

        is not `true`.


        **Filters.** `platform` restricts the read to one UGC platform (bare

        host or with `www.`); omit it for every UGC platform. `q` is a

        case-insensitive substring match on the group label or on any wording

        of the group''s sources in scope. `engines` and

        `tag_ids` narrow the checks the sources are read from. The window is

        `date_from` through `date_to`, inclusive, by the UTC day a check''s

        cycle started, defaulting to 30 days before today through today, and

        spans at most 3660 days; `applied` echoes the window read and the

        engines covered. `engines` is narrowed to the engines the workspace''s

        subscription includes, as on every other engine-filtering endpoint.

        `url_ids` is rejected with 422 `VALIDATION_ERROR` when sent.

        See [Filtering and sorting](/content/guides/filtering-and-sorting.html),

        [Pagination](/content/guides/pagination.html) and

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `VALIDATION_ERROR` | 422 | `url_ids` or `sort` was supplied, or `q` is empty
        or longer than 100 characters |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 3660 days |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: ugc_attributes_v1_projects__project_id__dashboard_ugc_attributes_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Restrict the response to one UGC platform's host, as returned
          in the `platform` field of the UGC impact ranking (e.g. `reddit.com`). A
          leading `www.` is optional. Omit it to read every UGC platform; a blank
          value is refused with `VALIDATION_ERROR` (422) rather than read as every
          platform.
        in: query
        name: platform
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Restrict the response to one UGC platform's host, as returned
            in the `platform` field of the UGC impact ranking (e.g. `reddit.com`).
            A leading `www.` is optional. Omit it to read every UGC platform; a blank
            value is refused with `VALIDATION_ERROR` (422) rather than read as every
            platform.
          title: Platform
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                analyzed_source_count: 61
                applied:
                  date_from: '2026-01-21'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - perplexity
                  - google_ai_overview
                data:
                - brand_mentioned_count: 5
                  label: pricing
                  platform_count: 2
                  source_count: 14
                  sources:
                  - engines:
                    - chatgpt
                    - perplexity
                    general_sentiment: positive
                    is_brand_mentioned: true
                    quotes: []
                    source_domain: reddit.com
                    source_title: Best rank tracking tools in 2026?
                    source_url: https://www.reddit.com/r/SEO/comments/1abc2de/best_rank_tracking_tools/
                    text: Pricing
                    wordings:
                    - quotes: []
                      text: Pricing
                    - quotes: []
                      text: Cost for the value
                  - engines:
                    - google_ai_overview
                    general_sentiment: null
                    is_brand_mentioned: null
                    quotes: []
                    source_domain: quora.com
                    source_title: Which AI search tracker is worth it?
                    source_url: https://www.quora.com/Which-AI-search-tracker-is-worth-it
                    text: pricing
                    wordings:
                    - quotes: []
                      text: pricing
                  sources_truncated: false
                - brand_mentioned_count: 3
                  label: ease of use
                  platform_count: 1
                  source_count: 9
                  sources:
                  - engines:
                    - perplexity
                    general_sentiment: null
                    is_brand_mentioned: false
                    quotes: []
                    source_domain: reddit.com
                    source_title: Where do AI Overviews pull citations from?
                    source_url: https://www.reddit.com/r/bigseo/comments/1xyz9ab/ai_overviews_citations/
                    text: ease of use
                    wordings:
                    - quotes: []
                      text: ease of use
                  sources_truncated: false
                limit: 50
                offset: 0
                total: 37
              schema:
                $ref: '#/components/schemas/UgcSignalListResponse'
          description: Paginated list of the attributes found on cited UGC sources,
            most widespread first. `total` counts attribute groups.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 3660 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 3660 days
                unsupported_filter:
                  summary: A parameter that does not apply to attributes
                  value:
                    code: VALIDATION_ERROR
                    error: filtering UGC attributes and voices by 'sort' is not supported;
                      a cited source is recorded against the prompt whose answer cited
                      it, not against a tracked URL, and the order is fixed by how
                      many sources carry the item
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Unsupported filter or sort, or a window that cannot be read
      summary: List UGC attributes
      tags:
      - UGC Impact
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-attributes?date_from=2026-01-21&date_to=2026-04-21&platform=reddit.com&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-attributes?date_from=2026-01-21&date_to=2026-04-21&platform=reddit.com&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-attributes\"\
          ,\n    params={\"date_from\": \"2026-01-21\", \"date_to\": \"2026-04-21\"\
          ,\n            \"platform\": \"reddit.com\", \"limit\": 50},\n    headers={\"\
          Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/dashboard/ugc-impact:
    get:
      description: 'Returns user-generated-content sources (Reddit, Quora, forums,
        review

        sites) cited by AI engines when answering your tracked prompts, sorted

        by impact. Use this to find the communities and threads driving your

        AI visibility, then prioritise outreach or content there.


        The window is exactly `date_from` through `date_to`, inclusive: a check

        counts when its cycle started (UTC) on one of those days, and never the

        day before `date_from`. A `date_to` later than today reads as today.


        `applied` echoes the window read and the engines the numbers cover: the

        requested `engines` the plan includes, or the plan''s engines when

        `engines` is omitted.


        Filter by `tag_ids` and `url_ids` to scope to a slice of prompts.

        Engine keys in the response are normalised to public canonical names.

        See [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: ugc_impact_v1_projects__project_id__dashboard_ugc_impact_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
        in: query
        name: url_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of URL IDs to filter by, each 1-2147483647.
          title: Url Ids
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-01-01'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                  - copilot
                data:
                - citations: 64
                  per_engine:
                    chatgpt: 32
                    copilot: 5
                    google_aio: 9
                    perplexity: 18
                  platform: reddit
                  share: 42.1
                - citations: 21
                  per_engine:
                    chatgpt: 12
                    copilot: 2
                    google_aio: 3
                    perplexity: 4
                  platform: quora
                  share: 13.8
                total: 152
              schema:
                $ref: '#/components/schemas/DashboardUgcImpactResponse'
          description: User-generated-content sources cited by AI engines.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date_to - date_from exceeds the maximum allowed range of
                      365 days.
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date window validation failed
      summary: Get UGC impact
      tags:
      - Dashboard
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-impact?date_from=2026-01-01&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-impact?date_from=2026-01-01&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-impact\"\
          ,\n    params={\"date_from\": \"2026-01-01\", \"date_to\": \"2026-04-21\"\
          },\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata =\
          \ res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/dashboard/ugc-impact/{platform}:
    get:
      description: 'Returns the individual sources behind one row of the UGC impact

        ranking: the actual Reddit threads, Quora answers, forum posts and

        review pages on that platform that AI engines cited when answering your

        tracked prompts. Read `/dashboard/ugc-impact` first to rank the

        platforms, then read this to see what is inside one of them.


        `platform` is the host as the ranking reports it (`reddit.com`); a

        leading `www.` is optional and both forms resolve to the same rows.


        **One row is one URL, not one citation.** Every check that cited a URL,

        by any engine, collapses into a single entry, so `engines` is the set of

        engines that cited it and `prompts` is every monitored prompt whose

        answers did. Paging this endpoint therefore pages URLs, and `total`

        counts URLs. URLs differing only by a trailing Chromium `#:~:text=`

        scroll-to-text fragment merge into the bare form, with

        `text_fragment_variant_count` recording how many distinct excerpts were

        seen.


        `general_sentiment` is null wherever `is_brand_mentioned` is not `true`.

        That is deliberate rather than missing data: sentiment is classified

        toward a mentioned brand, so a source that never names you has no

        sentiment to carry. `brand_awareness_score` and `sentiment_overview`

        describe the whole filtered set rather than the current page, so they do

        not move with `limit` and `offset` - but they do move with `engines`,

        `tag_ids` and `q`.


        **The window is `date_from` through `date_to`**, inclusive, by the UTC

        day a check''s cycle started, and defaults to 30 days before today

        through today, like every other windowed read; the rows, `total`,

        `brand_awareness_score` and `sentiment_overview` all follow it. A

        `date_to` later than today reads as today. The window spans at most

        3660 days, as far back as the deprecated `days` reaches, so moving off

        `days` loses no history. `days` reads the trailing

        `[today - days, today]` and cannot be combined with either date.

        `applied` echoes the window read and the engines covered.

        Filter with `engines`, `tag_ids`, and `q` for a case-insensitive

        substring match on the URL and on the covered subtopics. `url_ids` is not
        a parameter of

        this operation and is rejected with 422 `VALIDATION_ERROR` when sent: a

        cited source is recorded against the prompt whose answer cited it, not

        against a URL you track, so there is nothing for it to scope.


        `engines` is narrowed to the engines the workspace''s subscription

        includes, as on every other engine-filtering endpoint. Omit it and the

        response covers every included engine; a mixed filter drops the engines

        the subscription lacks, and a filter naming only engines it lacks

        answers `400 ENGINE_NOT_ENABLED`. Each row''s `engines` therefore lists

        included engines only.


        Every row carries a `source_kind`: `web` for an ordinary cited page,

        `shopping` for a Google Shopping product panel. A panel is not a link

        to a merchant and does not redirect to one, so its `source_domain` is

        always Google''s - read `source_title` for the product and the brand,

        never the domain. `source_kinds` filters on it and is the only filter

        of its kind on this API. It is opt-in and has no default: omit it and

        you get every surface, which is what every other count here reports.

        Nothing on this surface excludes panels on your behalf. Because

        `brand_awareness_score` and `sentiment_overview` describe the filtered

        set, they move with `source_kinds` as they do with `engines`.


        `has_analysis` keeps only the sources that have a page `summary`

        (`true`) or only those without one (`false`). It is applied with the

        brand and sentiment filters, so `total` follows it while

        `brand_awareness_score` and `sentiment_overview` do not.


        Sort on `source_url`, `is_brand_mentioned`, `general_sentiment` or

        `publication_date`; rows with a null value in the sorted field are last

        in either direction, and `source_url` ascending is always the tiebreak.

        See [Filtering and sorting](/content/guides/filtering-and-sorting.html),

        [Pagination](/content/guides/pagination.html) and

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `VALIDATION_ERROR` | 422 | `url_ids` was supplied, `sort` names a field
        outside the allowed set, `source_kinds` was empty or named an unknown surface,
        `days` was not a whole number from 1 to 3660, or `days` was sent with `date_from`
        or `date_to` |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 3660 days |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: ugc_platform_sources_v1_projects__project_id__dashboard_ugc_impact__platform__get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: The platform's host, as returned in the `platform` field of the
          UGC impact ranking (e.g. `reddit.com`). A leading `www.` is optional.
        in: path
        name: platform
        required: true
        schema:
          description: The platform's host, as returned in the `platform` field of
            the UGC impact ranking (e.g. `reddit.com`). A leading `www.` is optional.
          title: Platform
          type: string
      - description: Keep only sources that have a page summary (`true`) or only those
          that do not (`false`). Omit it for both. `brand_awareness_score` and `sentiment_overview`
          ignore it and describe the set before this filter.
        in: query
        name: has_analysis
        required: false
        schema:
          anyOf:
          - type: boolean
          - type: 'null'
          description: Keep only sources that have a page summary (`true`) or only
            those that do not (`false`). Omit it for both. `brand_awareness_score`
            and `sentiment_overview` ignore it and describe the set before this filter.
          title: Has Analysis
      - description: 'Comma-separated citation surfaces to restrict the response to:
          `web`, `shopping`, or both. Omit it for every surface - nothing on this
          API excludes a surface by default. `brand_awareness_score` and `sentiment_overview`
          are computed over the filtered set, so they move with this parameter.'
        in: query
        name: source_kinds
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Comma-separated citation surfaces to restrict the response
            to: `web`, `shopping`, or both. Omit it for every surface - nothing on
            this API excludes a surface by default. `brand_awareness_score` and `sentiment_overview`
            are computed over the filtered set, so they move with this parameter.'
          title: Source Kinds
      - deprecated: true
        description: 'Deprecated: use date_from/date_to. Trailing [today-days, today],
          1 to 3660. Cannot be combined with `date_from` or `date_to`.'
        in: query
        name: days
        required: false
        schema:
          anyOf:
          - maximum: 3660
            minimum: 1
            type: integer
          - type: 'null'
          deprecated: true
          description: 'Deprecated: use date_from/date_to. Trailing [today-days, today],
            1 to 3660. Cannot be combined with `date_from` or `date_to`.'
          title: Days
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-01-21'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - perplexity
                brand_awareness_score: 50.0
                data:
                - covered_subtopics:
                  - rank tracking
                  - reporting
                  detected_attributes:
                  - pricing
                  - ease of use
                  engines:
                  - chatgpt
                  - perplexity
                  general_sentiment: positive
                  is_brand_mentioned: true
                  prompts:
                  - engines:
                    - chatgpt
                    last_check_no: 37
                    last_cited_at: '2026-04-21T04:02:11Z'
                    prompt_id: 8814
                    text: best AI search visibility tools
                  publication_date: '2026-02-11'
                  source_domain: reddit.com
                  source_kind: web
                  source_title: Best rank tracking tools in 2026?
                  source_url: https://www.reddit.com/r/SEO/comments/1abc2de/best_rank_tracking_tools/
                  summary: Commenters compare five tools; two recommend ZipTie for
                    AI-search reporting and one raises pricing as a concern.
                  text_fragment_variant_count: 2
                - covered_subtopics:
                  - citations
                  detected_attributes: []
                  engines:
                  - perplexity
                  general_sentiment: null
                  is_brand_mentioned: null
                  prompts:
                  - engines:
                    - perplexity
                    last_check_no: 37
                    last_cited_at: '2026-04-21T04:02:44Z'
                    prompt_id: 8817
                    text: how do AI overviews choose sources
                  publication_date: null
                  source_domain: reddit.com
                  source_kind: web
                  source_title: Where do AI Overviews pull citations from?
                  source_url: https://www.reddit.com/r/bigseo/comments/1xyz9ab/ai_overviews_citations/
                  summary: null
                  text_fragment_variant_count: 0
                limit: 50
                offset: 0
                sentiment_overview:
                  negative_percentage: 0.0
                  neutral_percentage: 0.0
                  overall_score: 100.0
                  positive_percentage: 100.0
                total: 2
              schema:
                $ref: '#/components/schemas/UgcPlatformSourceListResponse'
          description: Paginated list of the individual sources this platform was
            cited from, one row per URL. `total` counts the URLs reachable by paging
            this endpoint.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 3660 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 3660 days
                days_with_dates:
                  summary: The deprecated days sent with a date
                  value:
                    code: VALIDATION_ERROR
                    error: days cannot be combined with date_from or date_to
                unsupported_filter:
                  summary: A filter that does not scope a cited source
                  value:
                    code: VALIDATION_ERROR
                    error: filtering UGC platform sources by url_ids is not supported;
                      a cited source is recorded against the prompt whose answer cited
                      it, not against a tracked URL
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Unsupported filter or sort field, or a window that cannot be
            read
      summary: List sources for a UGC platform
      tags:
      - Dashboard
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-impact/reddit.com?date_from=2026-01-21&date_to=2026-04-21&limit=50&sort=publication_date:desc\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-impact/reddit.com?date_from=2026-01-21&date_to=2026-04-21&limit=50&sort=publication_date:desc\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-impact/reddit.com\"\
          ,\n    params={\"date_from\": \"2026-01-21\", \"date_to\": \"2026-04-21\"\
          ,\n            \"limit\": 50, \"sort\": \"publication_date:desc\"},\n  \
          \  headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/dashboard/ugc-voices:
    get:
      description: 'Returns every strongly expressed user voice found on the UGC sources

        AI engines cited for your tracked prompts, grouped so each voice

        appears once with the sources it was found on. A voice is one line of

        the page analysis''s "strongly expressed user voice" section: what

        users say in their own words, often with a short verbatim quote.

        Sources analysed before this section existed carry no voices.


        **Grouping.** Voices are grouped by meaning: lines that express the

        same concern in different words form one group. Quoted fragments are

        lifted out of the line into `quotes` on each source. `label` is one

        member voice line as written, quotes included, with the list marker and

        emphasis marks removed, chosen as the group''s display text. Each source

        lists the distinct lines it carries in `wordings`, in the order the

        source lists them; on a voice, `wordings[].text` is the whole line as

        written there and `wordings[].quotes` its verbatim fragments. `text`

        and `quotes` on the source repeat the first wording.


        **Counting.** `source_count` is the number of distinct sources the

        voice appears on: a URL counts once however many checks, prompts or

        engines cited it, and URLs differing only by a trailing `#:~:text=`

        scroll-to-text fragment are one source. `platform_count` is the number

        of distinct platforms among them and `brand_mentioned_count` how many

        of the sources name one of your tracked brands. `analyzed_source_count`

        is the number of sources in scope that carry at least one voice, the

        denominator for `source_count`.


        **Order.** Groups come most widespread first (`source_count`

        descending, then `label` alphabetically). The order is fixed, so `sort`

        is not a parameter and is rejected with 422 `VALIDATION_ERROR` when

        sent. `total` counts groups after `q` and before paging. Each group

        lists its `sources`, newest check first and then by URL, capped at 100;

        `sources_truncated` is true when more carry the voice. On each source,

        `text` is the first line as written there and `quotes` the verbatim

        fragments it holds. `general_sentiment` is null wherever

        `is_brand_mentioned` is not `true`.


        **Filters.** `platform` restricts the read to one UGC platform (bare

        host or with `www.`); omit it for every UGC platform. `q` is a

        case-insensitive substring match on the group label or on any wording

        of the group''s sources in scope, quoted text included. `engines` and

        `tag_ids` narrow the checks the sources are read from. The window is

        `date_from` through `date_to`, inclusive, by the UTC day a check''s

        cycle started, defaulting to 30 days before today through today, and

        spans at most 3660 days; `applied` echoes the window read and the

        engines covered. `engines` is narrowed to the engines the workspace''s

        subscription includes, as on every other engine-filtering endpoint.

        `url_ids` is rejected with 422 `VALIDATION_ERROR` when sent.

        See [Filtering and sorting](/content/guides/filtering-and-sorting.html),

        [Pagination](/content/guides/pagination.html) and

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `VALIDATION_ERROR` | 422 | `url_ids` or `sort` was supplied, or `q` is empty
        or longer than 100 characters |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 3660 days |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: ugc_voices_v1_projects__project_id__dashboard_ugc_voices_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Restrict the response to one UGC platform's host, as returned
          in the `platform` field of the UGC impact ranking (e.g. `reddit.com`). A
          leading `www.` is optional. Omit it to read every UGC platform; a blank
          value is refused with `VALIDATION_ERROR` (422) rather than read as every
          platform.
        in: query
        name: platform
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Restrict the response to one UGC platform's host, as returned
            in the `platform` field of the UGC impact ranking (e.g. `reddit.com`).
            A leading `www.` is optional. Omit it to read every UGC platform; a blank
            value is refused with `VALIDATION_ERROR` (422) rather than read as every
            platform.
          title: Platform
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                analyzed_source_count: 23
                applied:
                  date_from: '2026-01-21'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - perplexity
                  - google_ai_overview
                data:
                - brand_mentioned_count: 2
                  label: 'Setup took far longer than the docs promised: "I lost a
                    whole afternoon on onboarding"'
                  platform_count: 2
                  source_count: 6
                  sources:
                  - engines:
                    - chatgpt
                    general_sentiment: neutral
                    is_brand_mentioned: true
                    quotes:
                    - I lost a whole afternoon on onboarding
                    source_domain: reddit.com
                    source_title: Best rank tracking tools in 2026?
                    source_url: https://www.reddit.com/r/SEO/comments/1abc2de/best_rank_tracking_tools/
                    text: 'Setup took far longer than the docs promised: "I lost a
                      whole afternoon on onboarding"'
                    wordings:
                    - quotes:
                      - I lost a whole afternoon on onboarding
                      text: 'Setup took far longer than the docs promised: "I lost
                        a whole afternoon on onboarding"'
                    - quotes:
                      - nothing worked on the first try
                      text: 'Onboarding was slow: "nothing worked on the first try"'
                  sources_truncated: false
                limit: 50
                offset: 0
                total: 12
              schema:
                $ref: '#/components/schemas/UgcSignalListResponse'
          description: Paginated list of the strongly expressed user voices found
            on cited UGC sources, most widespread first. `total` counts voice groups.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 3660 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 3660 days
                unsupported_filter:
                  summary: A parameter that does not apply to voices
                  value:
                    code: VALIDATION_ERROR
                    error: filtering UGC attributes and voices by 'sort' is not supported;
                      a cited source is recorded against the prompt whose answer cited
                      it, not against a tracked URL, and the order is fixed by how
                      many sources carry the item
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Unsupported filter or sort, or a window that cannot be read
      summary: List UGC user voices
      tags:
      - UGC Impact
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-voices?date_from=2026-01-21&date_to=2026-04-21&q=onboarding\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-voices?date_from=2026-01-21&date_to=2026-04-21&q=onboarding\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/dashboard/ugc-voices\"\
          ,\n    params={\"date_from\": \"2026-01-21\", \"date_to\": \"2026-04-21\"\
          , \"q\": \"onboarding\"},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/gsc/pages:
    get:
      description: 'Returns Google Search Console pages for the project''s connected

        property, paginated and sorted. Use this to enrich AI-citation data

        with traditional organic search metrics (impressions, clicks, CTR,

        position) at the page level.


        Page-level data includes the traffic from anonymized queries, which

        Google withholds from query-level data, so a page''s clicks and

        impressions are at least as large as the query-level totals from

        `/gsc/queries` and `/gsc/trends`. `ctr` is a 0-1 fraction. `position`

        is server-computed as the impressions-weighted mean of Google''s daily

        average position, so recomputing it from rounded daily values gives

        slightly different figures. `clicks_delta` and `impressions_delta`

        compare the window with the equal-length window just before

        `date_from`, and are null when the page has no stored data there.


        The data is limited to the country recorded on the project when the

        property was connected. Changing the project''s country later does not

        change it.


        The window is exactly `date_from` through `date_to`, inclusive, by UTC

        calendar day, and defaults to 30 days before today through today. A

        `date_to` later than today reads as today. On paid plans stored

        history reaches back about 16 months once the history load has

        finished; on trials it starts about 30 days before the property was

        connected. Dates before the stored history return empty results, not

        an error, and `applied.history_from` is the oldest day held. Spans

        over 365 days are refused with `DATE_RANGE_TOO_LARGE`. `applied`

        echoes the window read, the `property` read and `history_from`; its

        `engines` is always null, because Search Console data is not split by

        AI engine.


        A project can have several connected properties, one owner per domain.

        These reads show one property at a time, because a query row cannot be

        split by domain. Without `property` they read the primary property

        (the earliest connected), so a caller that never sends it keeps

        reading one property after a second is connected. Send `property`

        (the exact string, `sc-domain:example.com` or a URL prefix) to read

        another; a property with no active connection on the project is

        refused with 404 `NOT_FOUND`. A disconnected property reads as not

        connected while its stored history is kept. For figures across

        properties read `GET /v1/projects/{project_id}/website/pages`.


        If GSC is not connected for the project, the response contains

        `gsc_connected: false` and an empty data array rather than an error,

        making it safe to call unconditionally. The same holds when the

        property is disconnected while the request is being served. See

        [Pagination](/content/guides/pagination.html) and

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | `property` names a property the project has no active
        connection for |

        | `GSC_REAUTH_REQUIRED` | 409 | Google token revoked or expired; reconnect
        the property in the ziptie.ai app. Retrying will not help |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `UPSTREAM_ERROR` | 500 | Search Console data came back in an unexpected
        shape |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: gsc_pages_v1_projects__project_id__gsc_pages_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Which connected Search Console property to read, exactly as Google
          names it (`sc-domain:example.com` or a URL prefix such as `https://blog.example.com/`).
          Omit it to read the project's primary property, the earliest connected one,
          so a caller that never sends it keeps reading one property after a second
          is connected. A property the project has no active connection for is refused
          with 404 `NOT_FOUND`. Query rows cannot be split by domain, so these reads
          show one property at a time; for figures across properties read the website
          endpoints.
        in: query
        name: property
        required: false
        schema:
          anyOf:
          - maxLength: 2048
            type: string
          - type: 'null'
          description: Which connected Search Console property to read, exactly as
            Google names it (`sc-domain:example.com` or a URL prefix such as `https://blog.example.com/`).
            Omit it to read the project's primary property, the earliest connected
            one, so a caller that never sends it keeps reading one property after
            a second is connected. A property the project has no active connection
            for is refused with 404 `NOT_FOUND`. Query rows cannot be split by domain,
            so these reads show one property at a time; for figures across properties
            read the website endpoints.
          title: Property
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines: null
                  history_from: '2025-06-02'
                  property: sc-domain:example.com
                data:
                - clicks: 412
                  clicks_delta: 38
                  ctr: 0.05
                  impressions: 8240
                  impressions_delta: 610
                  page: https://example.com/best-running-shoes
                  position: 4.1
                - clicks: 188
                  clicks_delta: null
                  ctr: 0.0368
                  impressions: 5102
                  impressions_delta: null
                  page: https://example.com/review/trail-runners
                  position: 6.8
                gsc_connected: true
                limit: 50
                offset: 0
                total: 124
              schema:
                $ref: '#/components/schemas/GscPagesResponse'
          description: Paginated list of Search Console pages for the project.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: The project has no active Search Console connection for that
                  property
          description: '`property` names a property the project has no active connection
            for'
        '409':
          content:
            application/json:
              example:
                code: GSC_REAUTH_REQUIRED
                error: The project's Google Search Console connection must be reconnected
                  in the ziptie.ai app
          description: The project's Google Search Console connection must be reconnected
        '422':
          content:
            application/json:
              example:
                code: DATE_RANGE_TOO_LARGE
                error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date range exceeds 365 days
      summary: List Search Console pages
      tags:
      - Search Console
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/gsc/pages?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/gsc/pages?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/gsc/pages\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"limit\": 50},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/gsc/queries:
    get:
      description: 'Returns Google Search Console queries for the project''s connected

        property, paginated and sorted. Use this to compare what users type

        into Google with the prompts they ask AI engines, or to feed a single

        query-and-page table that blends both data sources.


        This is query-level Search Console data. Google withholds anonymized

        queries from query-level data, so these totals are lower than the

        property''s site-wide clicks and impressions and lower than the sum of

        `/gsc/pages` rows. The list holds each day''s top 1,000 queries by

        clicks, so a long tail of rare queries is not listed and the rows can

        add up to less than the property''s totals. `ctr` is a 0-1 fraction.

        `position` is server-computed as the impressions-weighted mean of

        Google''s daily

        average position, so recomputing it from rounded daily values gives

        slightly different figures. `clicks_delta` and `impressions_delta`

        compare the window with the equal-length window just before

        `date_from`, and are null when the query has no stored data there.


        The data is limited to the country recorded on the project when the

        property was connected. Changing the project''s country later does not

        change it.


        The window is exactly `date_from` through `date_to`, inclusive, by UTC

        calendar day, and defaults to 30 days before today through today. A

        `date_to` later than today reads as today. On paid plans stored

        history reaches back about 16 months once the history load has

        finished; on trials it starts about 30 days before the property was

        connected. Dates before the stored history return empty results, not

        an error, and `applied.history_from` is the oldest day held. Spans

        over 365 days are refused with `DATE_RANGE_TOO_LARGE`. `applied`

        echoes the window read, the `property` read and `history_from`; its

        `engines` is always null, because Search Console data is not split by

        AI engine.


        A project can have several connected properties, one owner per domain.

        These reads show one property at a time, because a query row cannot be

        split by domain. Without `property` they read the primary property

        (the earliest connected), so a caller that never sends it keeps

        reading one property after a second is connected. Send `property`

        (the exact string, `sc-domain:example.com` or a URL prefix) to read

        another; a property with no active connection on the project is

        refused with 404 `NOT_FOUND`. A disconnected property reads as not

        connected while its stored history is kept. For figures across

        properties read `GET /v1/projects/{project_id}/website/pages`.


        If GSC is not connected for the project, the response contains

        `gsc_connected: false` and an empty data array rather than an error.

        The same holds when the property is disconnected while the request is

        being served. See [Pagination](/content/guides/pagination.html) and

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | `property` names a property the project has no active
        connection for |

        | `GSC_REAUTH_REQUIRED` | 409 | Google token revoked or expired; reconnect
        the property in the ziptie.ai app. Retrying will not help |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `UPSTREAM_ERROR` | 500 | Search Console data came back in an unexpected
        shape |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: gsc_queries_v1_projects__project_id__gsc_queries_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Which connected Search Console property to read, exactly as Google
          names it (`sc-domain:example.com` or a URL prefix such as `https://blog.example.com/`).
          Omit it to read the project's primary property, the earliest connected one,
          so a caller that never sends it keeps reading one property after a second
          is connected. A property the project has no active connection for is refused
          with 404 `NOT_FOUND`. Query rows cannot be split by domain, so these reads
          show one property at a time; for figures across properties read the website
          endpoints.
        in: query
        name: property
        required: false
        schema:
          anyOf:
          - maxLength: 2048
            type: string
          - type: 'null'
          description: Which connected Search Console property to read, exactly as
            Google names it (`sc-domain:example.com` or a URL prefix such as `https://blog.example.com/`).
            Omit it to read the project's primary property, the earliest connected
            one, so a caller that never sends it keeps reading one property after
            a second is connected. A property the project has no active connection
            for is refused with 404 `NOT_FOUND`. Query rows cannot be split by domain,
            so these reads show one property at a time; for figures across properties
            read the website endpoints.
          title: Property
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines: null
                  history_from: '2025-06-02'
                  property: sc-domain:example.com
                data:
                - clicks: 320
                  clicks_delta: -12
                  ctr: 0.0431
                  impressions: 7420
                  impressions_delta: 240
                  position: 5.2
                  query: best running shoes
                - clicks: 145
                  clicks_delta: null
                  ctr: 0.0452
                  impressions: 3210
                  impressions_delta: null
                  position: 4.7
                  query: running shoes for flat feet
                gsc_connected: true
                limit: 50
                offset: 0
                total: 87
              schema:
                $ref: '#/components/schemas/GscQueriesResponse'
          description: Paginated list of Search Console queries for the project.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: The project has no active Search Console connection for that
                  property
          description: '`property` names a property the project has no active connection
            for'
        '409':
          content:
            application/json:
              example:
                code: GSC_REAUTH_REQUIRED
                error: The project's Google Search Console connection must be reconnected
                  in the ziptie.ai app
          description: The project's Google Search Console connection must be reconnected
        '422':
          content:
            application/json:
              example:
                code: DATE_RANGE_TOO_LARGE
                error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date range exceeds 365 days
      summary: List Search Console queries
      tags:
      - Search Console
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/gsc/queries?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/gsc/queries?date_from=2026-03-22&date_to=2026-04-21&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/gsc/queries\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          , \"limit\": 50},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/gsc/trends:
    get:
      description: 'Returns daily Google Search Console impressions and clicks for
        the

        project''s connected property over the requested window. Use this to

        plot organic-search performance alongside the AI-engine trend lines

        from `/dashboard/llm-trends`.


        Each point is Google''s own daily total of impressions and clicks for

        the project''s country, so it includes queries Google anonymizes in

        query-level data. A day whose total has not been stored yet is summed

        from the stored queries instead.


        `data` holds one point per day that has stored data, oldest first.

        Days without data are left out rather than zero-filled, and the last

        2-3 days are usually missing because Google reports Search Console data

        with a lag. `total` is the number of points in `data`.


        The data is limited to the country recorded on the project when the

        property was connected. Changing the project''s country later does not

        change it.


        The window is exactly `date_from` through `date_to`, inclusive, by UTC

        calendar day, and defaults to 30 days before today through today. A

        `date_to` later than today reads as today. On paid plans stored

        history reaches back about 16 months once the history load has

        finished; on trials it starts about 30 days before the property was

        connected. Dates before the stored history return empty results, not

        an error, and `applied.history_from` is the oldest day held. Spans

        over 365 days are refused with `DATE_RANGE_TOO_LARGE`. `applied`

        echoes the window read, the `property` read and `history_from`; its

        `engines` is always null, because Search Console data is not split by

        AI engine.


        A project can have several connected properties, one owner per domain.

        These reads show one property at a time, because a query row cannot be

        split by domain. Without `property` they read the primary property

        (the earliest connected), so a caller that never sends it keeps

        reading one property after a second is connected. Send `property`

        (the exact string, `sc-domain:example.com` or a URL prefix) to read

        another; a property with no active connection on the project is

        refused with 404 `NOT_FOUND`. A disconnected property reads as not

        connected while its stored history is kept. For figures across

        properties read `GET /v1/projects/{project_id}/website/pages`.


        If GSC is not connected for the project, the response contains

        `gsc_connected: false` and an empty data array rather than an error.

        The same holds when the property is disconnected while the request is

        being served.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | `property` names a property the project has no active
        connection for |

        | `GSC_REAUTH_REQUIRED` | 409 | Google token revoked or expired; reconnect
        the property in the ziptie.ai app. Retrying will not help |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `UPSTREAM_ERROR` | 500 | Search Console data came back in an unexpected
        shape |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: gsc_trends_v1_projects__project_id__gsc_trends_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Which connected Search Console property to read, exactly as Google
          names it (`sc-domain:example.com` or a URL prefix such as `https://blog.example.com/`).
          Omit it to read the project's primary property, the earliest connected one,
          so a caller that never sends it keeps reading one property after a second
          is connected. A property the project has no active connection for is refused
          with 404 `NOT_FOUND`. Query rows cannot be split by domain, so these reads
          show one property at a time; for figures across properties read the website
          endpoints.
        in: query
        name: property
        required: false
        schema:
          anyOf:
          - maxLength: 2048
            type: string
          - type: 'null'
          description: Which connected Search Console property to read, exactly as
            Google names it (`sc-domain:example.com` or a URL prefix such as `https://blog.example.com/`).
            Omit it to read the project's primary property, the earliest connected
            one, so a caller that never sends it keeps reading one property after
            a second is connected. A property the project has no active connection
            for is refused with 404 `NOT_FOUND`. Query rows cannot be split by domain,
            so these reads show one property at a time; for figures across properties
            read the website endpoints.
          title: Property
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-04-13'
                  date_to: '2026-04-17'
                  engines: null
                  history_from: '2025-06-02'
                  property: sc-domain:example.com
                data:
                - clicks: 92
                  date: '2026-04-13'
                  impressions: 1840
                - clicks: 108
                  date: '2026-04-14'
                  impressions: 2010
                - clicks: 121
                  date: '2026-04-15'
                  impressions: 2230
                gsc_connected: true
                total: 3
              schema:
                $ref: '#/components/schemas/GscTrendsResponse'
          description: Aggregated impressions and clicks over the requested window.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: The project has no active Search Console connection for that
                  property
          description: '`property` names a property the project has no active connection
            for'
        '409':
          content:
            application/json:
              example:
                code: GSC_REAUTH_REQUIRED
                error: The project's Google Search Console connection must be reconnected
                  in the ziptie.ai app
          description: The project's Google Search Console connection must be reconnected
        '422':
          content:
            application/json:
              example:
                code: DATE_RANGE_TOO_LARGE
                error: date range exceeds 365 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date range exceeds 365 days
      summary: Get Search Console trends
      tags:
      - Search Console
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/gsc/trends?date_from=2026-03-22&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/gsc/trends?date_from=2026-03-22&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/gsc/trends\"\
          ,\n    params={\"date_from\": \"2026-03-22\", \"date_to\": \"2026-04-21\"\
          },\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata =\
          \ res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/limits:
    get:
      description: 'Returns the current API rate-limit configuration and live usage
        for the

        workspace this project belongs to. Use this to surface quota in your

        own UI, to warn users before they hit a limit, or to back off

        proactively from a worker.


        The response includes the configured per-minute and per-day caps in

        `rate_limits`, the live `day_used` counter (overlaid from Redis so it

        reflects in-flight traffic), and `day_remaining`. `day_used` counts

        one unit per `/v1/` request and one per MCP `tools/call`, this

        request included; MCP `initialize` and `tools/list` are not counted.


        `reset_at` is reserved for a future absolute reset timestamp and is

        always `null`. The daily counter resets at 00:00 UTC, so compute the

        next reset client-side rather than reading it from this field.


        `addon_active` is always `true` here and is not an entitlement check.

        A workspace whose API Access add-on is off never reaches this response

        body: the request is refused upstream with 403 `API_ADDON_INACTIVE`.

        Branch on that error code instead.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_limits_v1_projects__project_id__limits_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      responses:
        '200':
          content:
            application/json:
              example:
                addon_active: true
                day_remaining: 48720
                day_used: 1280
                rate_limits:
                  per_day: 50000
                  per_minute: 60
                  workspace_per_minute: 600
                reset_at: null
              schema:
                $ref: '#/components/schemas/LimitsResponse'
          description: Current rate-limit configuration and usage for the project.
            `reset_at` is reserved and always null; `addon_active` is always true
            and is not an entitlement check.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get rate limits
      tags:
      - Limits
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/limits\" \\\n  -H\
          \ \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/limits\"\
          , {\n  headers: { Authorization: \"Bearer zt_live_...\" }\n});\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/limits\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/prompt-suggestions:
    get:
      description: 'Returns the PENDING AI-generated prompt suggestions for the project,

        paginated and sorted. Accepted and dismissed rows are excluded. Use

        this to surface a suggestion inbox in your UI, or to let an MCP agent

        review the queue and accept the suggestions that match its workflow.


        Gated by `source_impact_enabled` on the workspace''s subscription;

        returns 404 NOT_FOUND when the flag is off.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: prompt_suggestions_list
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - cluster_name: Brand monitoring across LLMs
                  created_at: '2026-06-07T09:12:08Z'
                  evidence_topic_ids:
                  - 318
                  - 327
                  - 341
                  id_cluster: 42
                  id_prompt_suggestion: 91
                  suggested_text: What are the best tools to monitor brand mentions
                    across ChatGPT and Perplexity?
                limit: 50
                offset: 0
                total: 4
              schema:
                $ref: '#/components/schemas/PromptSuggestionListResponse'
          description: Pending suggestions on this page.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List pending prompt suggestions
      tags:
      - UGC Impact
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/prompt-suggestions/{id_prompt_suggestion}/accept:
    post:
      description: 'Converts a pending suggestion into a tracked prompt. Use this
        to add

        the suggested query into the project''s monitoring rotation, optionally

        rewriting the text with `text_override` and applying tag labels with

        `tag_ids`. The new prompt starts being checked on the next scheduled

        cycle.


        Returns 201 with the newly created prompt body and a `Location` header

        pointing at the prompt''s detail URL. Idempotent retries: a second

        accept call against the same suggestion returns 409 ALREADY_RESOLVED

        - treat that as success on retry.


        **A suggestion whose text already names a prompt in the project is

        refused with the same 409 ALREADY_RESOLVED**, under the comparison

        `POST /v1/projects/{project_id}/prompts` documents: leading and trailing

        whitespace removed, internal whitespace runs collapsed to one space,

        lower-cased, and within this project only. It is applied to

        `text_override` when you send one, so rewriting a suggestion into the

        wording of a prompt you already track is refused rather than tracked and

        billed twice. Nothing is created, no allowance is spent and the

        `tag_ids` you sent are not applied. Unlike the retry case above the

        suggestion is left pending, so dismiss it if you do not want it offered

        again.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INSUFFICIENT_CREDITS` | 402 | Workspace prompt-credit cap hit. |

        | `SUBSCRIPTION_READ_ONLY` | 403 | The workspace subscription grants no write
        access; renew it to accept suggestions. |

        | `NOT_FOUND` | 404 | Suggestion missing, in another project, or gate disabled.
        |

        | `ALREADY_RESOLVED` | 409 | Suggestion already accepted or dismissed, or
        its text matches a prompt the project already holds. |

        | `VALIDATION_ERROR` | 422 | `id_prompt_suggestion` is outside 1-2147483647,
        `text_override` is shorter than 10 or longer than 300 characters, `tag_ids`
        holds more than 100 ids or an id outside 1-2147483647, or the body was refused
        as invalid upstream. |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: prompt_suggestions_accept
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Suggestion identifier.
        in: path
        name: id_prompt_suggestion
        required: true
        schema:
          description: Suggestion identifier.
          maximum: 2147483647
          minimum: 1
          title: Id Prompt Suggestion
          type: integer
      requestBody:
        content:
          application/json:
            example:
              tag_ids:
              - 12
              - 47
              text_override: What's the best tool for tracking brand mentions in ChatGPT?
            schema:
              $ref: '#/components/schemas/AcceptSuggestionBody'
      responses:
        '201':
          content:
            application/json:
              example:
                created_at: '2026-06-07T10:01:45Z'
                id_lite_query: 8412
                id_prompt_suggestion: 91
                text: What's the best tool for tracking brand mentions in ChatGPT?
              schema:
                $ref: '#/components/schemas/AcceptSuggestionResponse'
          description: The newly created tracked prompt.
        '402':
          content:
            application/json:
              example:
                code: INSUFFICIENT_CREDITS
                error: insufficient prompt credits
          description: Workspace prompt-credit cap reached
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: not found
          description: Suggestion missing, in another project, or the gate is disabled
        '409':
          content:
            application/json:
              example:
                code: ALREADY_RESOLVED
                error: Suggestion has already been resolved.
          description: Conflict - suggestion already accepted or dismissed
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Accept a prompt suggestion
      tags:
      - UGC Impact
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions/91/accept\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -d '{\"text_override\":\"What is the best tool?\"\
          ,\"tag_ids\":[12,47]}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions/91/accept\"\
          ,\n  {\n    method: \"POST\",\n    headers: {\n      Authorization: \"Bearer\
          \ zt_live_...\",\n      \"Content-Type\": \"application/json\",\n    },\n\
          \    body: JSON.stringify({\n      text_override: \"What is the best tool?\"\
          ,\n      tag_ids: [12, 47],\n    }),\n  }\n);\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions/91/accept\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n    json={\n\
          \        \"text_override\": \"What is the best tool?\",\n        \"tag_ids\"\
          : [12, 47],\n    },\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/prompt-suggestions/{id_prompt_suggestion}/dismiss:
    post:
      description: 'Marks a pending suggestion dismissed. Non-billable. Use this to
        clear

        the suggestion from the inbox without accepting it; the suggestion

        persists so the generation pipeline avoids re-proposing the same

        text. Returns 204 No Content on success.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | Suggestion missing, in another project, or gate disabled.
        |

        | `ALREADY_RESOLVED` | 409 | Suggestion already accepted or dismissed. |

        | `VALIDATION_ERROR` | 422 | `id_prompt_suggestion` is outside 1-2147483647.
        |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: prompt_suggestions_dismiss
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Suggestion identifier.
        in: path
        name: id_prompt_suggestion
        required: true
        schema:
          description: Suggestion identifier.
          maximum: 2147483647
          minimum: 1
          title: Id Prompt Suggestion
          type: integer
      responses:
        '204':
          description: Empty response on successful dismissal.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: not found
          description: Suggestion missing, in another project, or the gate is disabled
        '409':
          content:
            application/json:
              example:
                code: ALREADY_RESOLVED
                error: Suggestion has already been resolved.
          description: Conflict - suggestion already accepted or dismissed
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Dismiss a prompt suggestion
      tags:
      - UGC Impact
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions/91/dismiss\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions/91/dismiss\"\
          ,\n  {\n    method: \"POST\",\n    headers: { Authorization: \"Bearer zt_live_...\"\
          \ },\n  }\n);\n// 204 No Content\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompt-suggestions/91/dismiss\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\n# 204 No\
          \ Content\n"
      x-required-scope: write
  /v1/projects/{project_id}/prompts:
    get:
      description: 'Returns the prompts tracked in a project, paginated and sorted.
        Use this

        to enumerate every query the project monitors against AI engines, to

        power a prompts table in your own dashboard, or to find a `prompt_id`

        before reading its check history.


        `monitoring_status`, `q` (a case-insensitive substring of the text,

        matched literally), `tag_ids`, `sort` and the page are all applied

        before the page is cut, so every prompt is reachable through `offset`

        and `total` counts the prompts under every filter. Engine keys in the

        response use the public canonical names.


        `tag_ids` keeps the prompts carrying any of the listed tags (at most

        100 ids). A tag id that is not one of the project''s matches nothing,

        so it answers an empty page rather than an error. Each prompt''s `tags`

        names the tags it carries.


        `sort` takes `last_check_date`, `times_checked` or `text` (default

        `last_check_date:desc`). A prompt never checked has a null

        `last_check_date`, which sorts first ascending and last descending;

        `times_checked` counts a never-checked prompt as 0, and `text` compares

        case-insensitively. Ties are broken by `id`, ascending.


        The list is not windowed and carries no per-engine answers, so it takes

        no `date_from`, `date_to`, `engines` or `url_ids`. Sending `engines` is

        refused with 422 `VALIDATION_ERROR` rather than answered with an

        unfiltered list; per-engine scores live on `GET .../prompts/performance`.


        Each prompt carries `intent` (`informational`, `commercial`,

        `transactional` or `navigational`) and `funnel_stage` (`awareness`,

        `consideration` or `decision`). Both are assigned automatically shortly

        after a prompt is added, and either is `null` while the prompt is

        unclassified. The list does not filter on them; filter the returned

        prompts in your own code.


        Each prompt also carries `google_organic_position` and

        `bing_organic_position`: the best organic rank a URL from the project''s

        domain held at the prompt''s most recent check, as the app''s Prompts

        table shows it. Google organic results are captured by `google_aio`

        checks and Bing organic results by `bing_ai_overview` checks. Either is

        `null` when the domain did not rank, the prompt has not been checked,

        or that check has no organic data. For one check''s full organic

        results, read the check (`GET .../prompts/{prompt_id}/checks/{check_no}`).

        For every prompt''s ranks at one check, read

        `GET .../checks/answers?check_no=N`.


        See [Filtering and sorting](/content/guides/filtering-and-sorting.html),

        [Pagination](/content/guides/pagination.html), and

        [Engine names](/content/guides/engine-names.html).


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_prompts_v1_projects__project_id__prompts_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: If set, return only prompts with this monitoring state.
        in: query
        name: monitoring_status
        required: false
        schema:
          anyOf:
          - type: boolean
          - type: 'null'
          description: If set, return only prompts with this monitoring state.
          title: Monitoring Status
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - bing_organic_position: 8
                  funnel_stage: consideration
                  google_organic_position: 3
                  id: 12345
                  intent: commercial
                  last_check_date: '2026-04-19T06:00:00Z'
                  monitoring_frequency: 7
                  monitoring_status: true
                  tags:
                  - product
                  - comparison
                  text: best running shoes for flat feet
                  times_checked: 14
                - bing_organic_position: null
                  funnel_stage: null
                  google_organic_position: null
                  id: 12346
                  intent: null
                  last_check_date: '2026-04-19T06:00:00Z'
                  monitoring_frequency: 7
                  monitoring_status: true
                  tags:
                  - category
                  text: top rated CRM for small business
                  times_checked: 12
                limit: 50
                offset: 0
                total: 142
              schema:
                $ref: '#/components/schemas/PromptListResponse'
          description: Paginated list of prompts in the project.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List prompts
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/prompts?monitoring_status=true&tag_ids=3,8&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/prompts?monitoring_status=true&tag_ids=3,8&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts\"\
          ,\n    params={\"monitoring_status\": \"true\", \"tag_ids\": \"3,8\", \"\
          limit\": 50},\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n\
          )\ndata = res.json()\n"
      x-required-scope: read
    post:
      description: 'Creates a new prompt under the project and returns it with the
        assigned

        `id`. Use this when a user adds a query to track, when seeding a project

        from an import, or when bulk-loading prompts from your own product. The

        prompt enters the monitoring rotation immediately when

        `monitoring_status` is true.


        **Cadence is the project''s, not the prompt''s.** A monitored prompt is

        checked on the project''s `check_frequency_days` (`GET

        /v1/projects/{project_id}`), and the `monitoring_frequency` on the

        response echoes that same value. There is no request field for it and

        sending one returns 422: a per-prompt cadence would name a schedule the

        project does not run and an allowance it does not spend.


        **A prompt that differs only in case or in spacing is the same

        prompt.** The text you send is compared against every prompt already in

        the project through a comparison key: leading and trailing whitespace

        removed, every run of internal whitespace collapsed to one space, and

        the result lower-cased. The key decides the comparison and is never

        stored, so a matched prompt keeps the text it was created with and your

        spelling does not replace it. A match creates nothing and spends no

        prompt allowance, so `Acme  CRM` and `acme crm` are one prompt and one

        deduction. The status code separates the two outcomes and is what to

        branch on: `201` created a prompt, `409 PROMPT_ALREADY_EXISTS` matched an

        existing one and names it in `existing_id`.


        Comparison is per project, so the same text in two projects is two

        prompts and two deductions. `existing_id` can be `null` on that 409:

        naming the matched prompt takes a second read, over the whole project

        with the same rule, and a failed read is not retried. The 409 itself

        still means the text matched, so read it as "already tracked" and

        locate the prompt with `GET /v1/projects/{project_id}/prompts?q=`. The

        allowance check runs ahead of

        the match, so a monitored create from a workspace with no allowance left

        is refused with 402 even where the text would have matched and cost

        nothing.


        Send a unique `Idempotency-Key` header to make the call safely

        retryable; replays return the original response with

        `X-Idempotent-Replay: true`. Reusing the same key with a different body

        returns 409.

        Only a successful answer is stored and replayed; a refused request is

        not, so a retry with the same key runs it again.

        Plan caps are enforced before creation and return 402 when exceeded.


        **Only a monitored create is measured against the cap.** A create with

        `monitoring_status: false` claims no slot, so it is not checked against

        the allowance and never returns 402, even in a workspace with none

        left. The slot is claimed later, if you turn monitoring on with `PATCH

        /v1/projects/{project_id}/prompts/{prompt_id}`, and that is where the

        402 arrives instead.


        See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `PROMPT_ALREADY_EXISTS` | 409 | The project already holds a prompt whose
        text matches the one sent, compared with case and internal spacing ignored.
        `existing_id` names it where the follow-up lookup reaches it |

        | `PROMPT_LIMIT_REACHED` | 402 | The workspace''s monitored-prompt allowance
        for the cadence this project runs on is spent. Only for `monitoring_status:
        true`; a paused create is never refused with it |

        | `INSUFFICIENT_CREDITS` | 402 | Workspace prompt allowance for the period
        is spent. Only for `monitoring_status: true` |

        | `SUBSCRIPTION_READ_ONLY` | 403 | The workspace subscription grants no write
        access; renew it to add prompts |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |


        A prompt `402` body (`PROMPT_LIMIT_REACHED` or `INSUFFICIENT_CREDITS`)

        also carries `check_frequency`, `cap`, `held`, `resets_at` and `reason`,

        naming the exhausted window, its cap, how many prompts hold a slot in

        it, when it resets, and why the slot is refused. A `402` means the

        monitored prompt count itself would exceed the cap, so `reason` is

        `monitored` and waiting for the reset does not help; pause or remove a

        prompt, or move to a longer cadence. A window full only of prompts

        already checked in it does not refuse the create: the create succeeds

        and the response''s `checks_start_at` reports when the prompt is first

        checked. `reason` is additive and may be absent; a client should

        treat any other value as a refusal too.


        **Deferred checks.** When the cadence''s window is full of prompts

        already checked in it, but the monitored count still fits the cap, a

        `monitoring_status: true` create succeeds and the prompt''s checks are

        held until the window resets. The response carries that instant as

        `checks_start_at` (ISO-8601 UTC), which is `null` when the prompt is

        checked on the normal schedule. Only a future instant is reported.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: create_prompt_v1_projects__project_id__prompts_post
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              monitoring_status: true
              tag_ids:
              - 3
              - 8
              text: best running shoes for flat feet
              url_ids:
              - 12
            schema:
              $ref: '#/components/schemas/CreatePromptBody'
        required: true
      responses:
        '201':
          content:
            application/json:
              example:
                checks_start_at: null
                id: 12345
                monitoring_frequency: 7
                monitoring_status: true
                text: best running shoes for flat feet
              schema:
                $ref: '#/components/schemas/PromptCreateResponse'
          description: The newly created prompt.
        '402':
          content:
            application/json:
              examples:
                insufficient_credits:
                  summary: The monitored prompt count itself would exceed the cap,
                    so waiting for the reset does not help
                  value:
                    cap: 100
                    check_frequency: 30
                    code: INSUFFICIENT_CREDITS
                    error: Your monthly prompt limit is reached.
                    held: 40
                    reason: monitored
                    resets_at: '2026-10-01T00:00:00Z'
                prompt_limit_reached:
                  summary: The monitored prompt count would exceed the cadence's cap
                    (cap pre-check)
                  value:
                    cap: 10
                    check_frequency: 1
                    code: PROMPT_LIMIT_REACHED
                    error: daily prompts cap reached
                    held: 10
                    reason: monitored
                    resets_at: '2026-09-25T00:00:00Z'
          description: Plan cap reached
        '409':
          content:
            application/json:
              examples:
                idempotency_key_reused:
                  summary: Same Idempotency-Key with different body
                  value:
                    code: IDEMPOTENCY_KEY_REUSED
                    error: The provided Idempotency-Key has already been used with
                      a different request body.
                prompt_already_exists:
                  summary: The text matches a prompt already in the project
                  value:
                    code: PROMPT_ALREADY_EXISTS
                    error: A prompt with this text already exists in this project.
                    existing_id: 12345
          description: Conflict - the text matches a prompt the project already holds,
            or idempotency-key reuse
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Create prompt
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/prompts\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          text\":\"best running shoes for flat feet\",\"monitoring_status\":true,\"\
          tag_ids\":[3,8],\"url_ids\":[12]}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/prompts\"\
          , {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({\n    text: \"best\
          \ running shoes for flat feet\",\n    monitoring_status: true,\n    tag_ids:\
          \ [3, 8],\n    url_ids: [12],\n  }),\n});\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\n  \
          \      \"text\": \"best running shoes for flat feet\",\n        \"monitoring_status\"\
          : True,\n        \"tag_ids\": [3, 8],\n        \"url_ids\": [12],\n    },\n\
          )\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/prompts/performance:
    get:
      description: "Returns the project's prompts as the app's Prompts table shows\
        \ them:\neach prompt's AI Score, its change over the window, its score trend,\n\
        and every engine's answer at its latest check. Use this to rank prompts\n\
        by how well the brand does in AI answers, to find the prompts that\ndeclined,\
        \ lost a citation or that no engine cites, or to sync the Prompts table into\n\
        your own reporting. For the summary card above that table, read\n`GET .../prompts/summary`\
        \ with the same filters.\n\n**Scores are percentages on a 0-100 scale**, at\
        \ one decimal.\n`success_score` is the prompt's AI Score at its latest check\
        \ on or\nbefore `date_to`: the mean over the engines in `applied.engines`\
        \ that\nanswered that check, so a failed engine is left out rather than counted\n\
        as 0, and the score is `null` when no engine answered.\n`success_score_trend`\
        \ is index-aligned with `check_dates`, one value\nper check day, `null` on\
        \ a day the prompt has no check or no engine\nscored. `success_score_delta`\
        \ is the score change over the window: the\nlast measured point of the trend\
        \ minus the first in the window, in\npoints, and `null` with fewer than two.\n\
        \n`engines` holds one entry per engine in `applied.engines`, keyed by\npublic\
        \ name: whether the answer cited the project's domain and mentioned\nits brand,\
        \ its sentiment, the brand it favoured most, and `failed`.\n`failed` is `true`\
        \ when the engine's run failed (its answer fields are\nthen `null`) and `null`\
        \ when the engine has no row at that check.\n\nThe window is exactly `date_from`\
        \ through `date_to`, inclusive, and\ndefaults to the 30 days before today\
        \ (UTC) through today, the app's\n30-day view. A check is dated by the UTC\
        \ day its cycle started. The rows\nare the monitored prompts with a check\
        \ in the window plus every paused\nprompt. With a `date_to` before today,\
        \ each row reads the prompt's\nlatest check on or before `date_to`, as the\
        \ app does for a past range.\nA `date_to` later than today reads as today.\
        \ `applied` echoes the window\nand the engines the scores cover.\n\nFilters,\
        \ all applied before the page is cut, so `total` counts the\nprompts under\
        \ every filter:\n- `engines` narrows the scores, `engines`, and the `any`\
        \ engine of\n  `changes` and `leader_engine` to those engines (the ones the\n\
        \  workspace's plan includes).\n- `tag_ids` keeps prompts carrying any of\
        \ the tags (at most 100 ids).\n- `monitoring_status` keeps monitored (`true`)\
        \ or paused (`false`)\n  prompts.\n- `q` keeps prompts whose text contains\
        \ it, case-insensitively.\n- `signal_filters`, `score_min`, `score_max`, `score_change`,\n\
        \  `changes`, `intents`, `funnel_stages`, `leader_engine` with\n  `leader_brands`,\
        \ and `organic` are the app's Prompts filters; see the\n  parameters. Different\
        \ filters are combined with AND.\n\n`sort` takes `success_score`, `success_score_delta`,\
        \ `last_check_date`\nor `text` (default `success_score:desc`). A `null` `success_score`\
        \ or\n`last_check_date` sorts last in both directions. A `null`\n`success_score_delta`\
        \ sorts as 0, the order the app shows. `text`\ncompares the text as stored,\
        \ case-sensitively.\n\nSee [Working with prompts](/content/guides/working-with-prompts.html),\n\
        [Filtering and sorting](/content/guides/filtering-and-sorting.html),\n[Pagination](/content/guides/pagination.html),\
        \ and\n[Engine names](/content/guides/engine-names.html).\n\n**Errors specific\
        \ to this endpoint:**\n\n| Code | HTTP | When |\n|------|------|------|\n\
        | `INVALID_ENGINE_NAME` | 400 | `engines`, `signal_filters`, `changes` or\
        \ `leader_engine` names an engine that does not exist |\n| `ENGINE_NOT_ENABLED`\
        \ | 400 | Every engine in `engines` is one the workspace's plan does not include;\
        \ `signal_filters`, `changes` or `leader_engine` names an engine the plan\
        \ does not include; or `organic` reads `google` without `google_aio` (or `bing`\
        \ without `bing_ai_overview`) on the plan |\n| `NOT_FOUND` | 404 | The project\
        \ was deleted while the request was being answered |\n| `DATE_RANGE_TOO_LARGE`\
        \ | 422 | `date_to - date_from` exceeds 365 days |\n| `VALIDATION_ERROR` |\
        \ 422 | A malformed date or `date_from` after today; a `sort` field not listed\
        \ above (the body lists `allowed`); a malformed `signal_filters` rule, an\
        \ unknown signal, more than 35 rules or 1000 characters, or a rule engine\
        \ outside `engines`; a filter value outside its grammar or range (`score_min`\
        \ above `score_max`, a malformed `score_change` or `organic`, more than 28\
        \ `changes` tokens or an unknown kind, an unknown intent or funnel stage,\
        \ `leader_brands` without `leader_engine` or over its limits); a `changes`\
        \ or `leader_engine` engine outside `engines`; more than 100 `tag_ids`; `url_ids`\
        \ sent |\n\nSee [error codes](/content/error-codes.html) for the standard\
        \ errors (401, 402, 403, 422, 429, 500, 503).\n"
      operationId: list_prompt_performance_v1_projects__project_id__prompts_performance_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: If set, keep only monitored (`true`) or only paused (`false`)
          prompts.
        in: query
        name: monitoring_status
        required: false
        schema:
          anyOf:
          - type: boolean
          - type: 'null'
          description: If set, keep only monitored (`true`) or only paused (`false`)
            prompts.
          title: Monitoring Status
      - description: Comma-separated rules `[+|-]<engine>:<signal>`, all of which
          a prompt must satisfy; `-` keeps the prompts the rule does not match. `<engine>`
          is a public engine name and `<signal>` one of `cited`, `mentioned`, `sentiment_positive`,
          `sentiment_mixed` or `sentiment_negative`, read at the prompt's latest check
          in the window. A rule on an engine whose check failed matches neither way.
          At most 35 rules and 1000 characters. With `engines` set, every rule must
          name one of them.
        in: query
        name: signal_filters
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated rules `[+|-]<engine>:<signal>`, all of which
            a prompt must satisfy; `-` keeps the prompts the rule does not match.
            `<engine>` is a public engine name and `<signal>` one of `cited`, `mentioned`,
            `sentiment_positive`, `sentiment_mixed` or `sentiment_negative`, read
            at the prompt's latest check in the window. A rule on an engine whose
            check failed matches neither way. At most 35 rules and 1000 characters.
            With `engines` set, every rule must name one of them.
          title: Signal Filters
      - description: Keep prompts whose AI Score (`success_score`) is at least this
          many points, 0-100. A prompt with a `null` score never matches. Must not
          be above `score_max`.
        in: query
        name: score_min
        required: false
        schema:
          anyOf:
          - maximum: 100
            minimum: 0
            type: integer
          - type: 'null'
          description: Keep prompts whose AI Score (`success_score`) is at least this
            many points, 0-100. A prompt with a `null` score never matches. Must not
            be above `score_max`.
          title: Score Min
      - description: Keep prompts whose AI Score (`success_score`) is at most this
          many points, 0-100. A prompt with a `null` score never matches.
        in: query
        name: score_max
        required: false
        schema:
          anyOf:
          - maximum: 100
            minimum: 0
            type: integer
          - type: 'null'
          description: Keep prompts whose AI Score (`success_score`) is at most this
            many points, 0-100. A prompt with a `null` score never matches.
          title: Score Max
      - description: 'Keep prompts by `success_score_delta`, the score change over
          the window (the last measured point minus the first), in points: `down:N`
          fell by at least N, `up:N` rose by at least N (N 1-100), `within:N` moved
          by at most N either way (N 0-100). A prompt with a `null` change never matches.'
        in: query
        name: score_change
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Keep prompts by `success_score_delta`, the score change over
            the window (the last measured point minus the first), in points: `down:N`
            fell by at least N, `up:N` rose by at least N (N 1-100), `within:N` moved
            by at most N either way (N 0-100). A prompt with a `null` change never
            matches.'
          title: Score Change
      - description: Comma-separated tokens `<kind>:<engine>`, all of which a prompt
          must match. `<kind>` is one of `lost_mention`, `lost_citation`, `gained_mention`,
          `gained_citation`. A mention is lost when the engine's latest answered check
          in the window does not mention the brand and the answered check before it
          did, and gained the other way round; a citation follows the same rule on
          the project's domain being cited. A failed check is skipped. `<engine>`
          is a public engine name, or `any` for any engine in `applied.engines`. At
          most 28 tokens. With `engines` set, a named engine must be one of them.
        in: query
        name: changes
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated tokens `<kind>:<engine>`, all of which a prompt
            must match. `<kind>` is one of `lost_mention`, `lost_citation`, `gained_mention`,
            `gained_citation`. A mention is lost when the engine's latest answered
            check in the window does not mention the brand and the answered check
            before it did, and gained the other way round; a citation follows the
            same rule on the project's domain being cited. A failed check is skipped.
            `<engine>` is a public engine name, or `any` for any engine in `applied.engines`.
            At most 28 tokens. With `engines` set, a named engine must be one of them.
          title: Changes
      - description: 'Comma-separated search intents; keeps prompts classified with
          any of them: `informational`, `commercial`, `transactional`, `navigational`.'
        in: query
        name: intents
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Comma-separated search intents; keeps prompts classified with
            any of them: `informational`, `commercial`, `transactional`, `navigational`.'
          title: Intents
      - description: 'Comma-separated funnel stages; keeps prompts classified with
          any of them: `awareness`, `consideration`, `decision`.'
        in: query
        name: funnel_stages
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Comma-separated funnel stages; keeps prompts classified with
            any of them: `awareness`, `consideration`, `decision`.'
          title: Funnel Stages
      - description: Keep prompts whose `favored_brand` at the latest check is a tracked
          competitor, never one of the project's own brands, on this engine (a public
          engine name) or, with `any`, on any engine in `applied.engines`. With `engines`
          set, a named engine must be one of them.
        in: query
        name: leader_engine
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Keep prompts whose `favored_brand` at the latest check is a
            tracked competitor, never one of the project's own brands, on this engine
            (a public engine name) or, with `any`, on any engine in `applied.engines`.
            With `engines` set, a named engine must be one of them.
          title: Leader Engine
      - description: 'Narrows `leader_engine` to these competitors: a JSON array of
          at most 20 names, each 1-200 characters, matched the way the app''s Competitors
          page groups brand spellings. Needs `leader_engine`.'
        in: query
        name: leader_brands
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Narrows `leader_engine` to these competitors: a JSON array
            of at most 20 names, each 1-200 characters, matched the way the app''s
            Competitors page groups brand spellings. Needs `leader_engine`.'
          title: Leader Brands
      - description: 'Keep prompts by the project''s organic search position at the
          prompt''s latest check: `google:top:N` (position 1 to N), `google:between:A:B`
          (A to B), `google:unranked` (not in the results), or the same with `bing`,
          where 1 <= N and 1 <= A <= B <= 100. `google` needs `google_aio` on the
          workspace''s plan and `bing` needs `bing_ai_overview`.'
        in: query
        name: organic
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Keep prompts by the project''s organic search position at
            the prompt''s latest check: `google:top:N` (position 1 to N), `google:between:A:B`
            (A to B), `google:unranked` (not in the results), or the same with `bing`,
            where 1 <= N and 1 <= A <= B <= 100. `google` needs `google_aio` on the
            workspace''s plan and `bing` needs `bing_ai_overview`.'
          title: Organic
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                check_dates:
                - '2026-04-07'
                - '2026-04-14'
                - '2026-04-21'
                data:
                - check_no: 42
                  engines:
                    chatgpt:
                      cited: true
                      failed: false
                      favored_brand: Acme
                      mentioned: true
                      response_status: null
                      sentiment: Positive
                    google_aio:
                      cited: false
                      failed: false
                      favored_brand: Brooks
                      mentioned: true
                      response_status: true
                      sentiment: Mixed
                    perplexity:
                      cited: null
                      failed: true
                      favored_brand: null
                      mentioned: null
                      response_status: null
                      sentiment: null
                  id: 12345
                  last_check_date: '2026-04-21T06:00:00+00:00'
                  monitoring_status: true
                  success_score: 66.7
                  success_score_delta: 16.7
                  success_score_trend:
                  - 50.0
                  - 58.3
                  - 66.7
                  tags:
                  - color: '#2563EB'
                    id: 3
                    name: product
                  text: best running shoes for flat feet
                - check_no: 42
                  engines:
                    chatgpt:
                      cited: false
                      failed: false
                      favored_brand: HubSpot
                      mentioned: true
                      response_status: null
                      sentiment: Negative
                    google_aio:
                      cited: false
                      failed: false
                      favored_brand: null
                      mentioned: false
                      response_status: false
                      sentiment: Unknown
                    perplexity:
                      cited: false
                      failed: false
                      favored_brand: Salesforce
                      mentioned: false
                      response_status: null
                      sentiment: Unknown
                  id: 12346
                  last_check_date: '2026-04-21T06:00:00+00:00'
                  monitoring_status: true
                  success_score: 40.0
                  success_score_delta: null
                  success_score_trend:
                  - null
                  - null
                  - 40.0
                  tags: []
                  text: top rated CRM for small business
                limit: 50
                offset: 0
                total: 142
              schema:
                $ref: '#/components/schemas/PromptPerformanceResponse'
          description: One page of the project's prompts with their AI Score, its
            change and trend, and each engine's answer, as the app's Prompts table
            shows them.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                filter_engine_not_enabled:
                  summary: changes, leader_engine or organic needs an engine off the
                    workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: 'A filter needs an engine the workspace''s plan does not
                      include: copilot'
                invalid_engine_name:
                  summary: engines, signal_filters, changes or leader_engine names
                    an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
                signal_engine_not_enabled:
                  summary: signal_filters names an engine off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: 'signal_filters names an engine the workspace''s plan does
                      not include: gemini'
          description: An engine name that does not exist, or an engine the workspace's
            plan does not include
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Project not found
          description: The project was deleted while the request was being answered
        '422':
          content:
            application/json:
              examples:
                bad_score_change:
                  summary: score_change is outside its grammar
                  value:
                    code: VALIDATION_ERROR
                    error: score_change must be down:N or up:N (N 1 to 100), or within:N
                      (N 0 to 100)
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                sort_not_allowed:
                  summary: A sort field this endpoint does not take
                  value:
                    allowed:
                    - last_check_date
                    - success_score
                    - success_score_delta
                    - text
                    code: VALIDATION_ERROR
                    error: sort field 'times_checked' not allowed
                unknown_signal:
                  summary: A signal_filters rule names an unknown signal
                  value:
                    code: VALIDATION_ERROR
                    error: signal_filters rule 'chatgpt:quoted' names an unknown signal;
                      use one of cited, mentioned, sentiment_mixed, sentiment_negative,
                      sentiment_positive
                url_ids_refused:
                  summary: url_ids is not a filter here
                  value:
                    code: VALIDATION_ERROR
                    error: filtering prompt performance by 'url_ids' is not supported;
                      a prompt's score is not scoped by URL
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: A filter, the sort or the date window was refused
      summary: List prompt performance
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/prompts/performance?date_from=2026-03-22&date_to=2026-04-21&changes=lost_citation:any&score_max=50&sort=success_score:asc\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const params = new URLSearchParams({\n  date_from: \"2026-03-22\"\
          ,\n  date_to: \"2026-04-21\",\n  changes: \"lost_citation:any\",\n  score_max:\
          \ \"50\",\n  sort: \"success_score:asc\",\n});\nconst res = await fetch(\n\
          \  `https://api.ziptie.ai/v1/projects/abc123/prompts/performance?${params}`,\n\
          \  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts/performance\"\
          ,\n    params={\n        \"date_from\": \"2026-03-22\",\n        \"date_to\"\
          : \"2026-04-21\",\n        \"changes\": \"lost_citation:any\",\n       \
          \ \"score_max\": 50,\n        \"sort\": \"success_score:asc\",\n    },\n\
          \    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/prompts/summary:
    get:
      description: 'Returns the summary card the app shows above its Prompts table:
        how

        many prompts the filters keep, their average AI Score with its change

        and trend, the failed checks behind it, and the best and worst prompt.

        Use this for a headline number next to `GET .../prompts/performance`,

        which takes the same `date_from`, `date_to`, `engines`, `tag_ids` and

        Prompts filters (`signal_filters`, `score_min`, `score_max`,

        `score_change`, `changes`, `intents`, `funnel_stages`, `leader_engine`,

        `leader_brands` and `organic`) and lists the prompts behind the card.


        **Scores are percentages on a 0-100 scale**, at one decimal.

        `average_success_score_trend` is index-aligned with `check_dates`: the

        mean AI Score of the active prompts measured that day, `null` on a day

        none was. `average_success_score` is its latest measured point and

        `average_success_score_delta` the change over the window: the last

        measured point minus the first in the window, in points. Paused prompts are
        counted in `total_count` and

        `paused_count` but do not enter the average.


        `failed_checks` counts the failed engine checks of the active prompts

        on the day `average_success_score` shows; `newer_failed_checks` and

        `newer_failed_at` report failures on later days where none of those

        prompts scored, so a score that looks current can say it is not.


        `best_prompt` is the active prompt with the highest latest score.

        `worst_prompt` is the one with the lowest, and is set only when that

        score is below 50 and the prompt is not `best_prompt`, as in the app.


        The window rule is the one `GET .../prompts/performance` uses: exactly

        `date_from` through `date_to`, inclusive, defaulting to the 30 days

        before today (UTC) through today. The card aggregates every prompt the

        filters keep, so it takes no `q`, `sort`, `limit` or `offset`; `q` is

        refused and the paging parameters are ignored. It takes no

        `monitoring_status` either, because the card counts active and paused

        prompts separately; a sent `monitoring_status` is ignored. `applied`

        echoes the window and the engines the scores cover.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines`, `signal_filters`, `changes` or
        `leader_engine` names an engine that does not exist |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include; `signal_filters`, `changes` or `leader_engine` names
        an engine the plan does not include; or `organic` reads `google` without `google_aio`
        (or `bing` without `bing_ai_overview`) on the plan |

        | `NOT_FOUND` | 404 | The project was deleted while the request was being
        answered |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |

        | `VALIDATION_ERROR` | 422 | A malformed date or `date_from` after today;
        a malformed `signal_filters` rule, an unknown signal, more than 35 rules or
        1000 characters, or a rule engine outside `engines`; a filter value outside
        its grammar or range (`score_min` above `score_max`, a malformed `score_change`
        or `organic`, more than 28 `changes` tokens or an unknown kind, an unknown
        intent or funnel stage, `leader_brands` without `leader_engine` or over its
        limits); a `changes` or `leader_engine` engine outside `engines`; more than
        100 `tag_ids`; `url_ids` or `q` sent |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_prompts_summary_v1_projects__project_id__prompts_summary_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Comma-separated rules `[+|-]<engine>:<signal>`, all of which
          a prompt must satisfy; `-` keeps the prompts the rule does not match. `<engine>`
          is a public engine name and `<signal>` one of `cited`, `mentioned`, `sentiment_positive`,
          `sentiment_mixed` or `sentiment_negative`, read at the prompt's latest check
          in the window. A rule on an engine whose check failed matches neither way.
          At most 35 rules and 1000 characters. With `engines` set, every rule must
          name one of them.
        in: query
        name: signal_filters
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated rules `[+|-]<engine>:<signal>`, all of which
            a prompt must satisfy; `-` keeps the prompts the rule does not match.
            `<engine>` is a public engine name and `<signal>` one of `cited`, `mentioned`,
            `sentiment_positive`, `sentiment_mixed` or `sentiment_negative`, read
            at the prompt's latest check in the window. A rule on an engine whose
            check failed matches neither way. At most 35 rules and 1000 characters.
            With `engines` set, every rule must name one of them.
          title: Signal Filters
      - description: Keep prompts whose AI Score (`success_score`) is at least this
          many points, 0-100. A prompt with a `null` score never matches. Must not
          be above `score_max`.
        in: query
        name: score_min
        required: false
        schema:
          anyOf:
          - maximum: 100
            minimum: 0
            type: integer
          - type: 'null'
          description: Keep prompts whose AI Score (`success_score`) is at least this
            many points, 0-100. A prompt with a `null` score never matches. Must not
            be above `score_max`.
          title: Score Min
      - description: Keep prompts whose AI Score (`success_score`) is at most this
          many points, 0-100. A prompt with a `null` score never matches.
        in: query
        name: score_max
        required: false
        schema:
          anyOf:
          - maximum: 100
            minimum: 0
            type: integer
          - type: 'null'
          description: Keep prompts whose AI Score (`success_score`) is at most this
            many points, 0-100. A prompt with a `null` score never matches.
          title: Score Max
      - description: 'Keep prompts by `success_score_delta`, the score change over
          the window (the last measured point minus the first), in points: `down:N`
          fell by at least N, `up:N` rose by at least N (N 1-100), `within:N` moved
          by at most N either way (N 0-100). A prompt with a `null` change never matches.'
        in: query
        name: score_change
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Keep prompts by `success_score_delta`, the score change over
            the window (the last measured point minus the first), in points: `down:N`
            fell by at least N, `up:N` rose by at least N (N 1-100), `within:N` moved
            by at most N either way (N 0-100). A prompt with a `null` change never
            matches.'
          title: Score Change
      - description: Comma-separated tokens `<kind>:<engine>`, all of which a prompt
          must match. `<kind>` is one of `lost_mention`, `lost_citation`, `gained_mention`,
          `gained_citation`. A mention is lost when the engine's latest answered check
          in the window does not mention the brand and the answered check before it
          did, and gained the other way round; a citation follows the same rule on
          the project's domain being cited. A failed check is skipped. `<engine>`
          is a public engine name, or `any` for any engine in `applied.engines`. At
          most 28 tokens. With `engines` set, a named engine must be one of them.
        in: query
        name: changes
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated tokens `<kind>:<engine>`, all of which a prompt
            must match. `<kind>` is one of `lost_mention`, `lost_citation`, `gained_mention`,
            `gained_citation`. A mention is lost when the engine's latest answered
            check in the window does not mention the brand and the answered check
            before it did, and gained the other way round; a citation follows the
            same rule on the project's domain being cited. A failed check is skipped.
            `<engine>` is a public engine name, or `any` for any engine in `applied.engines`.
            At most 28 tokens. With `engines` set, a named engine must be one of them.
          title: Changes
      - description: 'Comma-separated search intents; keeps prompts classified with
          any of them: `informational`, `commercial`, `transactional`, `navigational`.'
        in: query
        name: intents
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Comma-separated search intents; keeps prompts classified with
            any of them: `informational`, `commercial`, `transactional`, `navigational`.'
          title: Intents
      - description: 'Comma-separated funnel stages; keeps prompts classified with
          any of them: `awareness`, `consideration`, `decision`.'
        in: query
        name: funnel_stages
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Comma-separated funnel stages; keeps prompts classified with
            any of them: `awareness`, `consideration`, `decision`.'
          title: Funnel Stages
      - description: Keep prompts whose `favored_brand` at the latest check is a tracked
          competitor, never one of the project's own brands, on this engine (a public
          engine name) or, with `any`, on any engine in `applied.engines`. With `engines`
          set, a named engine must be one of them.
        in: query
        name: leader_engine
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Keep prompts whose `favored_brand` at the latest check is a
            tracked competitor, never one of the project's own brands, on this engine
            (a public engine name) or, with `any`, on any engine in `applied.engines`.
            With `engines` set, a named engine must be one of them.
          title: Leader Engine
      - description: 'Narrows `leader_engine` to these competitors: a JSON array of
          at most 20 names, each 1-200 characters, matched the way the app''s Competitors
          page groups brand spellings. Needs `leader_engine`.'
        in: query
        name: leader_brands
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Narrows `leader_engine` to these competitors: a JSON array
            of at most 20 names, each 1-200 characters, matched the way the app''s
            Competitors page groups brand spellings. Needs `leader_engine`.'
          title: Leader Brands
      - description: 'Keep prompts by the project''s organic search position at the
          prompt''s latest check: `google:top:N` (position 1 to N), `google:between:A:B`
          (A to B), `google:unranked` (not in the results), or the same with `bing`,
          where 1 <= N and 1 <= A <= B <= 100. `google` needs `google_aio` on the
          workspace''s plan and `bing` needs `bing_ai_overview`.'
        in: query
        name: organic
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Keep prompts by the project''s organic search position at
            the prompt''s latest check: `google:top:N` (position 1 to N), `google:between:A:B`
            (A to B), `google:unranked` (not in the results), or the same with `bing`,
            where 1 <= N and 1 <= A <= B <= 100. `google` needs `google_aio` on the
            workspace''s plan and `bing` needs `bing_ai_overview`.'
          title: Organic
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
        in: query
        name: tag_ids
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of tag IDs to filter by, each 1-2147483647.
          title: Tag Ids
      responses:
        '200':
          content:
            application/json:
              example:
                active_count: 130
                applied:
                  date_from: '2026-03-22'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                average_success_score: 54.2
                average_success_score_delta: 3.1
                average_success_score_trend:
                - 51.1
                - 52.6
                - 54.2
                best_prompt:
                  id: 12377
                  success_score: 91.7
                  text: crm with the best mobile app
                check_dates:
                - '2026-04-07'
                - '2026-04-14'
                - '2026-04-21'
                failed_checks: 4
                newer_failed_at: null
                newer_failed_checks: 0
                paused_count: 12
                total_count: 142
                worst_prompt:
                  id: 12391
                  success_score: 8.3
                  text: cheapest crm with email automation
              schema:
                $ref: '#/components/schemas/PromptsSummaryResponse'
          description: The project's prompt counts, average AI Score with its change
            and trend, failed checks, and best and worst prompt, as the app's Prompts
            summary card shows them.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                filter_engine_not_enabled:
                  summary: changes, leader_engine or organic needs an engine off the
                    workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: 'A filter needs an engine the workspace''s plan does not
                      include: copilot'
                invalid_engine_name:
                  summary: engines, signal_filters, changes or leader_engine names
                    an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
                signal_engine_not_enabled:
                  summary: signal_filters names an engine off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: 'signal_filters names an engine the workspace''s plan does
                      not include: gemini'
          description: An engine name that does not exist, or an engine the workspace's
            plan does not include
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Project not found
          description: The project was deleted while the request was being answered
        '422':
          content:
            application/json:
              examples:
                date_range_too_large:
                  summary: Date range exceeds 365 days
                  value:
                    code: DATE_RANGE_TOO_LARGE
                    error: date range exceeds 365 days
                leader_brands_without_leader_engine:
                  summary: leader_brands is sent without leader_engine
                  value:
                    code: VALIDATION_ERROR
                    error: leader_brands needs leader_engine
                q_refused:
                  summary: The card is not searched
                  value:
                    code: VALIDATION_ERROR
                    error: filtering the prompts summary by 'q' is not supported;
                      it aggregates every prompt the other filters keep
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: A filter or the date window was refused
      summary: Get prompts summary
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/prompts/summary?date_from=2026-03-22&date_to=2026-04-21&tag_ids=3,8&intents=commercial,transactional\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/prompts/summary?date_from=2026-03-22&date_to=2026-04-21&tag_ids=3,8&intents=commercial,transactional\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts/summary\"\
          ,\n    params={\n        \"date_from\": \"2026-03-22\",\n        \"date_to\"\
          : \"2026-04-21\",\n        \"tag_ids\": \"3,8\",\n        \"intents\": \"\
          commercial,transactional\",\n    },\n    headers={\"Authorization\": \"\
          Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/prompts/{prompt_id}:
    delete:
      description: 'Permanently deletes a prompt and stops further monitoring. Historical

        checks recorded against the prompt are not returned by the listing

        endpoints once the prompt is gone. The operation is idempotent:

        deleting an already-removed prompt still returns 204 with no body.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: delete_prompt_v1_projects__project_id__prompts__prompt_id__delete
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the prompt.
        in: path
        name: prompt_id
        required: true
        schema:
          description: Unique identifier of the prompt.
          maximum: 2147483647
          minimum: 1
          title: Prompt Id
          type: integer
      responses:
        '204':
          description: Empty response on successful deletion.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Delete prompt
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X DELETE \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\"\
          , {\n  method: \"DELETE\",\n  headers: { Authorization: \"Bearer zt_live_...\"\
          \ }\n});\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.delete(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\n"
      x-required-scope: admin
    get:
      description: 'Returns a single prompt with its tags, its linked URLs, the success

        score of each check in the `date_from` / `date_to` window (oldest

        first), and its `intent` and `funnel_stage`. Use this to render a prompt

        detail view, to get the tag ids a `PATCH` takes, or to confirm that a

        recently created prompt is being monitored. Per-engine results live on

        the check endpoints.


        The window defaults to 30 days before today through today (UTC) and

        spans at most 3660 days, so an early `date_from` returns the prompt''s

        whole check history, as the dashboard''s prompt detail shows it. A check

        is in the window when one of its engine answers was recorded on a day

        of it, as on the check listings. When no engine answered a check, its

        `success_score` is `null` and `answered_checks` is 0. `applied` echoes

        the window read and the plan''s engines the scores cover.


        `intent` and `funnel_stage` are assigned automatically shortly after the

        prompt is added and are `null` until then; a member with edit access can

        change them in the app.


        Engine keys in the response are normalised to the public canonical names

        (see [Engine names](/content/guides/engine-names.html)).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | Prompt ID does not exist or belongs to a different project
        |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 3660 days |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_prompt_detail_v1_projects__project_id__prompts__prompt_id__get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the prompt.
        in: path
        name: prompt_id
        required: true
        schema:
          description: Unique identifier of the prompt.
          maximum: 2147483647
          minimum: 1
          title: Prompt Id
          type: integer
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-20'
                  date_to: '2026-04-19'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                checks:
                - answered_checks: 6
                  check_no: 41
                  failed_checks: 0
                  success_score: 0.58
                  timestamp: '2026-04-12T06:00:00Z'
                - answered_checks: 5
                  check_no: 42
                  failed_checks: 1
                  success_score: 0.62
                  timestamp: '2026-04-19T06:00:00Z'
                funnel_stage: consideration
                id: 12345
                intent: commercial
                last_check_date: '2026-04-19T06:00:00Z'
                monitoring_frequency: 7
                monitoring_status: true
                tags:
                - color: '#2563EB'
                  id_tag: 3
                  name: product
                text: best running shoes for flat feet
                urls:
                - id_lite_url: 81
                  url: https://example.com/flat-feet-guide
              schema:
                $ref: '#/components/schemas/PromptDetailResponse'
          description: Detail of a single prompt with recent stats.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Prompt not found in project
          description: Prompt not found in this project
        '422':
          content:
            application/json:
              example:
                code: DATE_RANGE_TOO_LARGE
                error: date range exceeds 3660 days
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date range exceeds 3660 days
      summary: Get prompt
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\" \\\
          \n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\"\
          , {\n  headers: { Authorization: \"Bearer zt_live_...\" }\n});\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
    patch:
      description: 'Partially updates a prompt. Only the fields you send are changed:
        pass

        `monitoring_status` to pause or resume monitoring, `tag_ids` to replace

        the prompt''s tag associations, or `url_ids` to replace its URL

        associations. At least one field must be provided or the request returns

        422.


        Send an `Idempotency-Key` header to make the call safely retryable.

        Replays return the cached response with `X-Idempotent-Replay: true`.

        See [Idempotency](/content/guides/idempotency.html).


        The response carries the prompt''s `intent` and `funnel_stage`; neither

        can be set through this endpoint.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | Prompt ID does not exist or belongs to a different project
        |

        | `INSUFFICIENT_CREDITS` | 402 | Resuming monitoring spends prompt credits
        and the allowance is exhausted |

        | `SUBSCRIPTION_READ_ONLY` | 403 | The workspace subscription grants no write
        access; resuming monitoring is refused |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |


        The `402 INSUFFICIENT_CREDITS` body also carries `check_frequency`,

        `cap`, `held`, `resets_at` and `reason`, naming the exhausted window,

        its cap, how many prompts hold a slot in it, when it resets, and why:

        `reason` is `monitored` because the monitored prompt count itself would

        exceed the cap (waiting does not help). A resume into a window full only

        of prompts already checked in it succeeds instead, and the response''s

        `checks_start_at` (ISO-8601 UTC, `null` otherwise) reports when the

        prompt is first checked. `reason` is additive and may be absent, and

        a client should treat any other value as a refusal too.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: update_prompt_v1_projects__project_id__prompts__prompt_id__patch
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the prompt.
        in: path
        name: prompt_id
        required: true
        schema:
          description: Unique identifier of the prompt.
          maximum: 2147483647
          minimum: 1
          title: Prompt Id
          type: integer
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              monitoring_status: false
              tag_ids:
              - 3
              - 8
              - 14
            schema:
              $ref: '#/components/schemas/PatchPromptBody'
        required: true
      responses:
        '200':
          content:
            application/json:
              example:
                checks_start_at: null
                funnel_stage: consideration
                id: 12345
                intent: commercial
                monitoring_status: false
                text: best running shoes for flat feet
              schema:
                $ref: '#/components/schemas/PromptUpdateResponse'
          description: The updated prompt.
        '402':
          content:
            application/json:
              example:
                cap: 100
                check_frequency: 30
                code: INSUFFICIENT_CREDITS
                error: Your monthly prompt limit is reached.
                held: 40
                reason: monitored
                resets_at: '2026-10-01T00:00:00Z'
          description: Resuming monitoring would put the monitored prompt count over
            the cap
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Prompt not found in project
          description: Prompt not found in this project
        '409':
          content:
            application/json:
              example:
                code: IDEMPOTENCY_KEY_REUSED
                error: The provided Idempotency-Key has already been used with a different
                  request body.
          description: Idempotency-key reuse with different body
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Update prompt
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X PATCH \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          monitoring_status\":false,\"tag_ids\":[3,8,14]}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\"\
          , {\n  method: \"PATCH\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({ monitoring_status:\
          \ false, tag_ids: [3, 8, 14] }),\n});\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.patch(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"monitoring_status\"\
          : False, \"tag_ids\": [3, 8, 14]},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/prompts/{prompt_id}/checks:
    get:
      description: 'Returns the historical check runs recorded for a single prompt,

        paginated by `limit` and `offset` and bounded by the `date_from` /

        `date_to` window. Use this to plot how a prompt''s answer evolves over

        time, to power an audit log, or to find a specific `check_no` to fetch

        in detail. A check is in the window when one of its engine answers was

        recorded on a day of it (UTC); `applied` echoes the window and the

        engines read.


        **Failed checks.** An engine whose run of a check failed appears with

        `failed` true and null `cited` and `mentioned`, and is left out of the

        check''s `success_score`. When no engine answered a check, its

        `success_score` is `null` and `answered_checks` is 0.


        Engine keys in each check are normalised to the public canonical names.

        See [Pagination](/content/guides/pagination.html) and

        [Engine names](/content/guides/engine-names.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `NOT_FOUND` | 404 | Prompt ID does not exist or belongs to a different project
        |

        | `DATE_RANGE_TOO_LARGE` | 422 | `date_to - date_from` exceeds 365 days |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_prompt_checks_v1_projects__project_id__prompts__prompt_id__checks_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the prompt.
        in: path
        name: prompt_id
        required: true
        schema:
          description: Unique identifier of the prompt.
          maximum: 2147483647
          minimum: 1
          title: Prompt Id
          type: integer
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-01-01'
                  date_to: '2026-04-21'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                data:
                - answered_checks: 3
                  check_no: 14
                  engines:
                    chatgpt:
                      cited: true
                      failed: false
                      mentioned: true
                    google_aio:
                      cited: false
                      failed: false
                      mentioned: true
                    perplexity:
                      cited: true
                      failed: false
                      mentioned: true
                  failed_checks: 0
                  success_score: 0.8
                  timestamp: '2026-04-19T06:00:00Z'
                - answered_checks: 2
                  check_no: 13
                  engines:
                    chatgpt:
                      cited: true
                      failed: false
                      mentioned: false
                    google_aio:
                      cited: false
                      failed: false
                      mentioned: false
                    perplexity:
                      cited: null
                      failed: true
                      mentioned: null
                  failed_checks: 1
                  success_score: 0.6
                  timestamp: '2026-04-12T06:00:00Z'
                limit: 50
                offset: 0
                total: 14
              schema:
                $ref: '#/components/schemas/CheckListResponse'
          description: Paginated list of historical checks for the prompt.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Prompt not found in project
          description: Prompt not found in this project
        '422':
          content:
            application/json:
              example:
                code: DATE_RANGE_TOO_LARGE
                error: date_to - date_from exceeds the maximum allowed range of 365
                  days.
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Date range exceeds 365 days
      summary: List prompt checks
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks?limit=50&date_from=2026-01-01&date_to=2026-04-21\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks?limit=50&date_from=2026-01-01&date_to=2026-04-21\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks\"\
          ,\n    params={\"limit\": 50, \"date_from\": \"2026-01-01\", \"date_to\"\
          : \"2026-04-21\"},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/prompts/{prompt_id}/checks/latest:
    get:
      description: 'Returns the most recent check across all engines for a single
        prompt.

        Use this when you only care about the current state - for example, to

        render a "latest answer" panel, to badge a prompt with whether each

        engine currently mentions and cites your brand, or to sample answers

        without paging through history.


        The response has the same shape as

        `prompts/{prompt_id}/checks/{check_no}`: the brands you track, and per

        engine the answer text, whether your brand was mentioned and cited, the

        sentiment, every brand the answer named, and the cited sources. Engine

        keys are normalised to public canonical names. One difference: an

        engine with no row for this check is omitted rather than `null`, so

        every engine present carries a result.


        The response also carries four deprecated fields, kept so no `/v1`

        client loses a key it could read. `success_score` and per-engine

        `cited` / `mentioned` are always `null` here: they belong to the

        listing, `prompts/{prompt_id}/checks`, so read its first row with

        `limit=1` if you need them (per engine, `citations` and

        `brand_mention` in this response carry the same answers). The

        top-level `bing_ai_overview` object duplicates

        `engines.bing_ai_overview`; read that instead.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | Prompt ID does not exist or belongs to a different project
        |

        | `NOT_FOUND` | 404 | The prompt has no checks yet (a new prompt waits for
        the project''s next scheduled run) |


        A prompt added moments ago answers 404 until its first check lands, so

        a client polling for that first result treats the 404 as "not yet"

        rather than as a failure.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_latest_check_v1_projects__project_id__prompts__prompt_id__checks_latest_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the prompt.
        in: path
        name: prompt_id
        required: true
        schema:
          description: Unique identifier of the prompt.
          maximum: 2147483647
          minimum: 1
          title: Prompt Id
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                check_no: 14
                competitor_brands:
                - Rival Analytics
                - competitor.com
                engines:
                  chatgpt:
                    brand_mention: true
                    brands:
                    - brand: MyBrand
                      details:
                      - detail: Fast onboarding
                        sentiment: positive
                      order_of_mention: 1
                      prominence: high
                      sentiment: positive
                    - brand: Rival Analytics
                      details: []
                      order_of_mention: 2
                      prominence: medium
                      sentiment: neutral
                    citations: true
                    error_message: null
                    failed: false
                    favored_brand: MyBrand
                    followup_queries:
                    - Is MyBrand free for small teams?
                    response_length: 812
                    response_text: Several tools cover this. MyBrand is often cited
                      for...
                    sentiment: Positive
                    sources:
                    - domain: mybrand.com
                      position: 1
                      source_kind: web
                      source_title: null
                      url: https://mybrand.com/docs/getting-started
                    - domain: competitor.com
                      position: 2
                      source_kind: web
                      source_title: null
                      url: https://competitor.com/blog/comparison
                    subtopics:
                    - onboarding
                    - pricing
                  perplexity:
                    brand_mention: true
                    brands:
                    - brand: Rival Analytics
                      details: []
                      order_of_mention: 1
                      prominence: high
                      sentiment: positive
                    - brand: MyBrand
                      details: []
                      order_of_mention: 2
                      prominence: medium
                      sentiment: neutral
                    citations: false
                    error_message: null
                    failed: false
                    favored_brand: Rival Analytics
                    followup_queries: []
                    response_length: 640
                    response_text: Popular options include Rival Analytics and MyBrand...
                    sentiment: Neutral
                    sources:
                    - domain: competitor.com
                      position: 1
                      source_kind: web
                      source_title: null
                      url: https://competitor.com/pricing
                    subtopics:
                    - pricing
                timestamp: '2026-04-19T06:00:00Z'
                user_brand: MyBrand
              schema:
                $ref: '#/components/schemas/CheckLatestResponse'
          description: Most recent check for the prompt across engines.
        '404':
          content:
            application/json:
              examples:
                no_checks_yet:
                  summary: The prompt has no checks yet
                  value:
                    code: NOT_FOUND
                    error: No checks yet for this prompt
                prompt_not_found:
                  summary: Prompt not found in this project
                  value:
                    code: NOT_FOUND
                    error: Prompt not found in project
          description: Prompt not found in this project, or the prompt has no checks
            yet
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get latest check
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks/latest\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks/latest\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks/latest\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/prompts/{prompt_id}/checks/{check_no}:
    get:
      description: 'Returns a single historical check of one prompt, identified by
        its

        check number. Use this to deep-link to a specific run, to compare an old

        answer to today''s, or to render a side-by-side diff between two checks.


        Take `check_no` from a listing endpoint. Since June 2026 a check number

        is one project check cycle, shared by every prompt answered in that

        check, so a prompt''s numbers start at the first cycle it joined and can

        have gaps. Before June 2026 each prompt numbered its own checks 1, 2, 3.

        Engine keys in the response are normalised to public canonical names.

        To read the answers of many prompts at once, page `checks/answers`.


        This is the richest surface on the API and the one to reach for when

        you want the underlying evidence rather than an aggregate. Per engine

        it carries the answer text, whether your brand was mentioned and cited,

        the sentiment, every brand the answer named with its order and

        prominence, the discrete facts stated about each, and the ordered list

        of source URLs the engine drew from. An engine with no row for this

        check is `null` rather than absent, so you can tell "not checked" from

        "checked and found nothing".


        Note this differs from the listing endpoint, which reports only

        `{cited, mentioned}` per engine plus a combined `success_score`. The

        detail response has no `success_score`; compute or read it from the

        listing if you need it beside the evidence.


        Each source row carries a `source_kind`. `web` is an ordinary cited

        page. `shopping` is a Google Shopping product panel, which is not a

        link to a merchant and does not redirect to one, so its `domain` is

        always Google''s - read `source_title`, never its host, to learn whose

        product was shown. Panels are counted like any other citation

        everywhere on this API; treat the value set as open, since a kind you

        do not recognise is a surface added after your client was written.


        `source_title` is null wherever no title reached the row, which is the

        norm rather than the anomaly. `chatgpt`, `copilot` and `perplexity`

        cite without one, so every row from those three engines is null and no

        `shopping` row they return names its product. Every row analysed

        before titles were captured is null on all seven engines and cannot be

        backfilled, so expect null across the whole historical window. Read it

        as unknown rather than untitled.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | Prompt ID does not exist or belongs to a different project
        |

        | `NOT_FOUND` | 404 | The prompt has no check with this number |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_check_detail_v1_projects__project_id__prompts__prompt_id__checks__check_no__get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the prompt.
        in: path
        name: prompt_id
        required: true
        schema:
          description: Unique identifier of the prompt.
          maximum: 2147483647
          minimum: 1
          title: Prompt Id
          type: integer
      - description: Check number. Since June 2026 a check number is one project check
          cycle, shared by every prompt answered in that check, so a prompt's numbers
          start at the first cycle it joined and can have gaps. Before June 2026 each
          prompt numbered its own checks 1, 2, 3, so an older number is that prompt's
          own count.
        in: path
        name: check_no
        required: true
        schema:
          description: Check number. Since June 2026 a check number is one project
            check cycle, shared by every prompt answered in that check, so a prompt's
            numbers start at the first cycle it joined and can have gaps. Before June
            2026 each prompt numbered its own checks 1, 2, 3, so an older number is
            that prompt's own count.
          maximum: 2147483647
          minimum: 1
          title: Check No
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                check_no: 14
                competitor_brands:
                - Rival Analytics
                - competitor.com
                engines:
                  bing_ai_overview: null
                  chatgpt:
                    brand_mention: true
                    brands:
                    - brand: MyBrand
                      details:
                      - detail: Fast onboarding
                        sentiment: positive
                      order_of_mention: 1
                      prominence: high
                      sentiment: positive
                    - brand: Rival Analytics
                      details: []
                      order_of_mention: 2
                      prominence: medium
                      sentiment: neutral
                    citations: true
                    error_message: null
                    failed: false
                    favored_brand: MyBrand
                    followup_queries:
                    - Is MyBrand free for small teams?
                    response_length: 812
                    response_text: Several tools cover this. MyBrand is often cited
                      for...
                    sentiment: Positive
                    sources:
                    - domain: mybrand.com
                      position: 1
                      source_kind: web
                      source_title: null
                      url: https://mybrand.com/docs/getting-started
                    - domain: competitor.com
                      position: 2
                      source_kind: web
                      source_title: null
                      url: https://competitor.com/blog/comparison
                    - domain: www.google.com
                      position: 3
                      source_kind: shopping
                      source_title: null
                      url: https://www.google.com/search?q=rank+tracker&prds=pvt:hg,productid:1234567890&ibp=oshop
                    subtopics:
                    - onboarding
                    - pricing
                  copilot: null
                  gemini: null
                  google_ai_mode: null
                  google_aio: null
                  perplexity: null
                timestamp: '2026-04-19T06:00:00Z'
                user_brand: MyBrand
              schema:
                $ref: '#/components/schemas/CheckDetailResp'
          description: A specific historical check by sequence number, including per-brand
            extracted facts.
        '404':
          content:
            application/json:
              examples:
                check_not_found:
                  summary: The prompt has no check with this number
                  value:
                    code: NOT_FOUND
                    error: Check not found in project
                prompt_not_found:
                  summary: Prompt not found in this project
                  value:
                    code: NOT_FOUND
                    error: Prompt not found in project
          description: Prompt or check not found in this project
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get check
      tags:
      - Prompts
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks/14\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks/14\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/prompts/12345/checks/14\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/tags:
    get:
      description: 'Returns the tags defined for a project, paginated and sorted.
        Tags are

        colour-coded labels attached to prompts and URLs and are typically used

        to slice dashboard metrics by topic, funnel stage, or product line.


        `q` (a case-insensitive substring of the name, matched literally),

        `sort` and the page are applied before the page is cut, so every tag is

        reachable through `offset` and `total` counts the tags under `q`.

        `sort` takes `name` (case-insensitive) or `prompt_count` (default

        `name:asc`); ties are broken by `id`, ascending. Every tag carries its

        `color` as `#RRGGBB`.


        The list is not windowed and has no engine or tag axis, so it takes no

        `date_from`, `date_to`, `engines`, `tag_ids` or `url_ids`; sending

        `engines` or `tag_ids` is refused with 422 `VALIDATION_ERROR`.


        See [Filtering and sorting](/content/guides/filtering-and-sorting.html) and

        [Pagination](/content/guides/pagination.html).


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_tags_v1_projects__project_id__tags_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - color: '#3b82f6'
                  id: 3
                  name: product
                  prompt_count: 42
                - color: '#f59e0b'
                  id: 8
                  name: comparison
                  prompt_count: 17
                - color: '#10b981'
                  id: 14
                  name: brand
                  prompt_count: 9
                limit: 50
                offset: 0
                total: 12
              schema:
                $ref: '#/components/schemas/TagListResponse'
          description: Paginated list of tags in the project.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List tags
      tags:
      - Tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/tags?limit=50\" \\\
          \n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/tags?limit=50\"\
          , {\n  headers: { Authorization: \"Bearer zt_live_...\" }\n});\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/tags\"\
          ,\n    params={\"limit\": 50},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
    post:
      description: 'Creates a new tag with a display `name` and a `#RRGGBB` hex `color`.

        Use this when a user adds a label in your UI or when seeding a project

        with a standard taxonomy. The tag becomes available immediately for

        attachment to prompts and URLs.


        Tag names are unique in the project, ignoring case. A name the project

        already holds creates nothing and answers 409 `TAG_ALREADY_EXISTS`,

        whose `existing_id` names the tag that holds it.


        Send a unique `Idempotency-Key` header to make the call safely

        retryable; replays return the cached response with

        `X-Idempotent-Replay: true`.

        Only a successful answer is stored and replayed; a refused request is

        not, so a retry with the same key runs it again.

        Plan caps are enforced before creation and return 402 when exceeded.


        See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `TAG_ALREADY_EXISTS` | 409 | A tag with the same name, ignoring case, already
        exists in this project. `existing_id` names it |

        | `TAG_LIMIT_REACHED` | 402 | Project has reached its tag cap |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: create_tag_v1_projects__project_id__tags_post
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              color: '#f59e0b'
              name: comparison
            schema:
              $ref: '#/components/schemas/CreateTagBody'
        required: true
      responses:
        '201':
          content:
            application/json:
              example:
                color: '#f59e0b'
                id: 8
                name: comparison
                prompt_count: 0
              schema:
                $ref: '#/components/schemas/TagCreateResponse'
          description: The newly created tag.
        '402':
          content:
            application/json:
              example:
                code: TAG_LIMIT_REACHED
                error: Project has reached its tag cap. Upgrade your plan or delete
                  unused tags.
          description: Plan cap reached
        '409':
          content:
            application/json:
              examples:
                idempotency_key_reused:
                  summary: Same Idempotency-Key with different body
                  value:
                    code: IDEMPOTENCY_KEY_REUSED
                    error: The provided Idempotency-Key has already been used with
                      a different request body.
                tag_already_exists:
                  summary: Duplicate tag name
                  value:
                    code: TAG_ALREADY_EXISTS
                    error: A tag with this name already exists
                    existing_id: 8
          description: Conflict - duplicate tag name or idempotency-key reuse
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Create tag
      tags:
      - Tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/tags\" \\\
          \n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type: application/json\"\
          \ \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"name\":\"comparison\"\
          ,\"color\":\"#f59e0b\"}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/tags\"\
          , {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({ name: \"comparison\"\
          , color: \"#f59e0b\" }),\n});\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/tags\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"name\"\
          : \"comparison\", \"color\": \"#f59e0b\"},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/tags/{tag_id}:
    delete:
      description: 'Permanently deletes a tag and detaches it from every prompt and
        URL it

        was attached to. Prompts and URLs themselves are not deleted. The

        operation is idempotent: deleting an already-removed tag, or a tag id

        the project does not hold, still returns 204 with no body.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: delete_tag_v1_projects__project_id__tags__tag_id__delete
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the tag.
        in: path
        name: tag_id
        required: true
        schema:
          description: Unique identifier of the tag.
          maximum: 2147483647
          minimum: 1
          title: Tag Id
          type: integer
      responses:
        '204':
          description: Empty response on successful deletion.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Delete tag
      tags:
      - Tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X DELETE \"https://api.ziptie.ai/v1/projects/abc123/tags/8\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/tags/8\"\
          , {\n  method: \"DELETE\",\n  headers: { Authorization: \"Bearer zt_live_...\"\
          \ }\n});\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.delete(\n    \"https://api.ziptie.ai/v1/projects/abc123/tags/8\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\n"
      x-required-scope: admin
    patch:
      description: 'Partially updates a tag. Only the fields you send are changed:
        pass

        `name` to rename the tag or `color` to recolour it. At least one of the

        two must be provided or the request returns 422. The change is

        reflected immediately on every prompt and URL the tag is attached to.


        The response is the tag as stored after the update, read back, so it

        carries `name`, `color` and `prompt_count` whichever field you sent.

        Renaming a tag to its own name in another case is allowed; a name

        another tag of the project holds, ignoring case, is 409

        `TAG_ALREADY_EXISTS` with that tag''s `existing_id`.


        Send an `Idempotency-Key` header to make the call safely retryable.

        Only a successful answer is stored and replayed; a refused request is

        not, so a retry with the same key runs it again.

        See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | Tag ID does not exist or belongs to a different project
        |

        | `TAG_ALREADY_EXISTS` | 409 | Another tag of this project holds the new name,
        ignoring case. `existing_id` names it |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: update_tag_v1_projects__project_id__tags__tag_id__patch
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the tag.
        in: path
        name: tag_id
        required: true
        schema:
          description: Unique identifier of the tag.
          maximum: 2147483647
          minimum: 1
          title: Tag Id
          type: integer
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              color: '#ef4444'
              name: head-to-head
            schema:
              $ref: '#/components/schemas/PatchTagBody'
        required: true
      responses:
        '200':
          content:
            application/json:
              example:
                color: '#ef4444'
                id: 8
                name: head-to-head
                prompt_count: 17
              schema:
                $ref: '#/components/schemas/TagUpdateResponse'
          description: The updated tag.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Tag not found
          description: Tag not found in this project
        '409':
          content:
            application/json:
              examples:
                idempotency_key_reused:
                  summary: Same Idempotency-Key with different body
                  value:
                    code: IDEMPOTENCY_KEY_REUSED
                    error: The provided Idempotency-Key has already been used with
                      a different request body.
                tag_already_exists:
                  summary: New name conflicts with existing tag
                  value:
                    code: TAG_ALREADY_EXISTS
                    error: A tag with this name already exists
                    existing_id: 14
          description: Conflict - duplicate tag name or idempotency-key reuse
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Update tag
      tags:
      - Tags
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X PATCH \"https://api.ziptie.ai/v1/projects/abc123/tags/8\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          name\":\"head-to-head\",\"color\":\"#ef4444\"}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/tags/8\"\
          , {\n  method: \"PATCH\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({ name: \"head-to-head\"\
          , color: \"#ef4444\" }),\n});\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.patch(\n    \"https://api.ziptie.ai/v1/projects/abc123/tags/8\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"name\"\
          : \"head-to-head\", \"color\": \"#ef4444\"},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/topic-clusters:
    get:
      description: 'Returns the topic clusters surfaced for the project by the UGC
        Impact

        pipeline. Each cluster groups related conversation topics extracted

        from sources cited under the project''s monitored prompts. Use this to

        power a UGC overview, to drill into the topics behind each cluster,

        or to seed prompt suggestions.


        Gated by `source_impact_enabled` on the workspace''s subscription; the

        endpoint returns 404 NOT_FOUND when the flag is off.


        See [Filtering and sorting](/content/guides/filtering-and-sorting.html)

        and [Pagination](/content/guides/pagination.html).


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: topic_clusters_list
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - description: Discussions about tracking brand mentions in AI tools.
                  first_seen_check_no: 1
                  id_cluster: 42
                  last_seen_check_no: 17
                  name: Brand monitoring across LLMs
                  occurrence_count: 87
                  size: 12
                - description: User questions about subscription tiers and trial limits.
                  first_seen_check_no: 5
                  id_cluster: 43
                  last_seen_check_no: 17
                  name: Pricing
                  occurrence_count: 24
                  size: 6
                limit: 50
                offset: 0
                total: 5
              schema:
                $ref: '#/components/schemas/TopicClusterListResponse'
          description: Paginated list of topic clusters in the project.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List topic clusters
      tags:
      - UGC Impact
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/topic-clusters?limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/topic-clusters?limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/topic-clusters\"\
          ,\n    params={\"limit\": 50},\n    headers={\"Authorization\": \"Bearer\
          \ zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/topic-clusters/{cluster_id}/topics:
    get:
      description: 'Returns the topics that belong to a single cluster, paginated
        and

        sorted. Use this to drill into the conversation topics the cluster

        represents, to expose them as a sub-list in a UGC overview, or to

        find a `topic_id` before reading its source citations.


        Gated by `source_impact_enabled` on the workspace''s subscription;

        also 404s when `cluster_id` is unknown in this project.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: topic_clusters_topics_list
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the cluster.
        in: path
        name: cluster_id
        required: true
        schema:
          description: Unique identifier of the cluster.
          maximum: 2147483647
          minimum: 1
          title: Cluster Id
          type: integer
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - first_seen_check_no: 4
                  id_topic: 318
                  last_seen_check_no: 17
                  occurrence_count: 14
                  phrase: tracking brand mentions in LLMs
                - first_seen_check_no: 6
                  id_topic: 327
                  last_seen_check_no: 17
                  occurrence_count: 9
                  phrase: AI search citation analytics
                limit: 50
                offset: 0
                total: 12
              schema:
                $ref: '#/components/schemas/TopicListResponse'
          description: Paginated list of topics that belong to a cluster.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List topics in a cluster
      tags:
      - UGC Impact
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/topic-clusters/42/topics\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/topic-clusters/42/topics\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/topic-clusters/42/topics\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/topics/{topic_id}/sources:
    get:
      description: 'Returns the UGC sources cited under a topic, paginated and sorted.

        Use this to inspect the actual Reddit posts, YouTube videos, blog

        posts, and forum threads that the LLMs cited when discussing the

        topic. Each row carries the source URL and host, the page title where

        one was captured, the engine that cited it, how many times that engine

        cited it under this topic, when it was last seen, and a structured

        markdown summary.


        **Rows are grouped per engine**, so one URL appears once for each

        engine that cited it, each with its own `use_count`. Sum across rows

        for a URL to get its total, or group by `source_url` client-side.


        Cross-project enumeration is blocked: passing a `topic_id` that does

        not belong to `project_id` returns 404 NOT_FOUND. Also gated by

        `source_impact_enabled` on the workspace''s subscription. The `summary`

        and `source_title` fields are nullable for legacy rows persisted

        before those columns landed.


        `source_kind` names the citation surface: `web` for an ordinary cited

        page, `shopping` for a Google Shopping product panel. A panel''s

        `source_domain` is Google''s rather than the merchant''s, because it is

        not a link to one, so read `source_title` for the product. Rows of

        every kind are returned; this endpoint has no filter for it. Use

        `/dashboard/ugc-impact/{platform}` and its `source_kinds` parameter

        when you need one.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: topics_sources_list
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the topic.
        in: path
        name: topic_id
        required: true
        schema:
          description: Unique identifier of the topic.
          maximum: 2147483647
          minimum: 1
          title: Topic Id
          type: integer
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                data:
                - created_at: '2026-06-05T14:22:11Z'
                  engine: chatgpt
                  id_source_analysis: 12057
                  last_seen_at: '2026-06-05T14:22:11Z'
                  source_domain: reddit.com
                  source_kind: web
                  source_title: Best tools for a small SaaS team?
                  source_url: https://www.reddit.com/r/SaaS/comments/abc/best_tools/
                  summary: '## Dominant user concerns

                    Pricing tier confusion.'
                  use_count: 4
                limit: 50
                offset: 0
                total: 8
              schema:
                $ref: '#/components/schemas/UgcSourceListResponse'
          description: Paginated list of UGC sources cited under the topic.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List UGC sources for a topic
      tags:
      - UGC Impact
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/topics/318/sources\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/topics/318/sources\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/topics/318/sources\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/urls:
    get:
      description: 'Returns the URLs associated with a project, paginated and sorted.
        URLs

        are the pages you want tracked for AI-engine citations; counts of how

        often each URL is cited surface in the dashboard rollups.


        `q` (a case-insensitive substring of the URL, matched literally),

        `sort` and the page are applied before the page is cut, including a

        sort on `citation_rate` or `last_cited`, so every URL is reachable

        through `offset` and `total` counts the URLs under `q`. `sort` takes

        `url`, `prompt_count`, `citation_rate` or `last_cited` (default

        `citation_rate:desc`). A null `citation_rate` or `last_cited` sorts

        lowest: first ascending, last descending. Ties are broken by `id`,

        ascending. The app''s URLs table puts nulls last in both directions.


        `engines` narrows the engines each `citation_rate` covers: the rate is

        computed over the requested engines the workspace''s plan includes, or

        over the plan''s engines when `engines` is omitted. `applied.engines`

        echoes that set; `applied.date_from` and `applied.date_to` are null,

        because the list is not windowed. `last_cited` covers every engine

        whatever `engines` names.


        The list takes no `date_from`, `date_to` or `url_ids`. Sending `tag_ids`

        is refused with 422 `VALIDATION_ERROR`, because URL list items carry no

        tag associations.


        See [Filtering and sorting](/content/guides/filtering-and-sorting.html) and

        [Pagination](/content/guides/pagination.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_urls_v1_projects__project_id__urls_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: null
                  date_to: null
                  engines:
                  - chatgpt
                  - perplexity
                data:
                - citation_rate: 0.82
                  id: 12
                  last_cited: '2026-04-19'
                  prompt_count: 8
                  url: https://example.com/best-running-shoes
                - citation_rate: 0.44
                  id: 13
                  last_cited: '2026-04-17'
                  prompt_count: 5
                  url: https://example.com/review/trail-runners
                limit: 50
                offset: 0
                total: 36
              schema:
                $ref: '#/components/schemas/UrlListResponse'
          description: Paginated list of URLs in the project.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: List URLs
      tags:
      - URLs
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/urls?engines=chatgpt,perplexity&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/urls?engines=chatgpt,perplexity&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/urls\"\
          ,\n    params={\"engines\": \"chatgpt,perplexity\", \"limit\": 50},\n  \
          \  headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
    post:
      description: 'Registers a new URL with the project and returns it with the assigned

        `id`. URLs must use http or https, have a valid host and be at most

        1024 characters; a longer one is refused with 422 `VALIDATION_ERROR`.

        Use this when a user adds a page to track or when bulk-importing pages

        from a sitemap.


        A URL the project already tracks, compared as the exact string, is

        not created again: the request answers 409 `URL_ALREADY_EXISTS`, whose

        `existing_id` names the tracked URL.


        Send a unique `Idempotency-Key` header to make the call safely

        retryable.

        Only a successful answer is stored and replayed; a refused request is

        not, so a retry with the same key runs it again.

        Plan caps are enforced before creation and return 402 when exceeded.


        See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `URL_ALREADY_EXISTS` | 409 | The project already tracks this exact URL.
        `existing_id` names it |

        | `URL_LIMIT_REACHED` | 402 | Project has reached its URL cap |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: create_url_v1_projects__project_id__urls_post
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              url: https://example.com/best-running-shoes
            schema:
              $ref: '#/components/schemas/CreateUrlBody'
        required: true
      responses:
        '201':
          content:
            application/json:
              example:
                id: 12
                prompt_count: 0
                url: https://example.com/best-running-shoes
              schema:
                $ref: '#/components/schemas/UrlCreateResponse'
          description: The newly created URL.
        '402':
          content:
            application/json:
              example:
                code: URL_LIMIT_REACHED
                error: Project has reached its URL cap. Upgrade your plan or delete
                  unused URLs.
          description: Plan cap reached
        '409':
          content:
            application/json:
              examples:
                idempotency_key_reused:
                  summary: Same Idempotency-Key with different body
                  value:
                    code: IDEMPOTENCY_KEY_REUSED
                    error: The provided Idempotency-Key has already been used with
                      a different request body.
                url_already_exists:
                  summary: The project already tracks this exact URL
                  value:
                    code: URL_ALREADY_EXISTS
                    error: A URL with this value already exists
                    existing_id: 12
          description: Conflict - the URL is already tracked, or idempotency-key reuse
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Create URL
      tags:
      - URLs
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X POST \"https://api.ziptie.ai/v1/projects/abc123/urls\" \\\
          \n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type: application/json\"\
          \ \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"url\":\"https://example.com/best-running-shoes\"\
          }'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/urls\"\
          , {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({ url: \"https://example.com/best-running-shoes\"\
          \ }),\n});\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.post(\n    \"https://api.ziptie.ai/v1/projects/abc123/urls\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"url\"\
          : \"https://example.com/best-running-shoes\"},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/urls/{url_id}:
    delete:
      description: 'Permanently deletes a URL and removes its associations to every
        prompt.

        Historical citations against the URL are no longer returned by the

        listing endpoints once the URL is gone. It also disconnects every

        content piece in the app that was connected to the URL, as deleting

        the URL in the app does; the pieces themselves are kept. The app''s

        dashboards and Action Center reflect the delete as they do an app

        delete. The operation is idempotent:

        deleting an already-removed URL, or a URL id the project does not hold,

        still returns 204 with no body.


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: delete_url_v1_projects__project_id__urls__url_id__delete
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the URL.
        in: path
        name: url_id
        required: true
        schema:
          description: Unique identifier of the URL.
          maximum: 2147483647
          minimum: 1
          title: Url Id
          type: integer
      responses:
        '204':
          description: Empty response on successful deletion.
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Delete URL
      tags:
      - URLs
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X DELETE \"https://api.ziptie.ai/v1/projects/abc123/urls/12\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/urls/12\"\
          , {\n  method: \"DELETE\",\n  headers: { Authorization: \"Bearer zt_live_...\"\
          \ }\n});\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.delete(\n    \"https://api.ziptie.ai/v1/projects/abc123/urls/12\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\n"
      x-required-scope: admin
    get:
      description: 'Returns a single URL with its citation rollup over the requested
        date

        window (derived from `date_from` and `date_to`). Use this to render a

        URL detail view, to verify a freshly registered URL is in the

        monitoring set, or to see which linked prompts cite it on which engine.


        The window is exactly `date_from` through `date_to`, inclusive: a check

        counts when its cycle started (UTC) on one of those days, and never the

        day before `date_from`. A `date_to` later than today reads as today.


        `applied` echoes the window read and the engines the numbers cover: the

        requested `engines` the plan includes, or the plan''s engines when

        `engines` is omitted.


        `trend` is the URL''s citation rate at each check date, a fraction from

        0 to 1, over the answered engine checks. A failed check is not an

        answer, and neither is a `google_aio` or `bing_ai_overview` run that

        showed no overview; a date with no answered check is left out of

        `trend` and `check_dates` alike. `failed_checks` counts the failed

        checks left out, and each linked prompt''s `failed_engines` names the

        engines whose latest check on or before `date_to` failed. A linked

        prompt''s `engines` leaves out an engine with no check on that date, and a

        `google_aio` or `bing_ai_overview` engine whose run that date showed

        no overview.


        Engine keys in the response are normalised to public canonical names

        (see [Engine names](/content/guides/engine-names.html)).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `ENGINE_NOT_ENABLED` | 400 | Every engine in `engines` is one the workspace''s
        plan does not include |

        | `NOT_FOUND` | 404 | URL ID does not exist or belongs to a different project
        |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_url_detail_v1_projects__project_id__urls__url_id__get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the URL.
        in: path
        name: url_id
        required: true
        schema:
          description: Unique identifier of the URL.
          maximum: 2147483647
          minimum: 1
          title: Url Id
          type: integer
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: '2026-03-23'
                  date_to: '2026-04-22'
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                check_dates:
                - '2026-04-01'
                - '2026-04-08'
                - '2026-04-22'
                failed_checks: 4
                id: 12
                linked_prompts:
                - cited: true
                  engines:
                    chatgpt: true
                    google_aio: false
                    perplexity: null
                  failed_engines:
                  - perplexity
                  id: 12345
                  last_cited_at: '2026-04-22T06:00:00Z'
                  text: best running shoes for flat feet
                - cited: false
                  engines:
                    chatgpt: false
                    google_aio: false
                    perplexity: false
                  failed_engines: []
                  id: 12388
                  last_cited_at: null
                  text: top trail runners for beginners
                trend:
                - 0.5
                - 0.67
                - 0.75
                url: https://example.com/best-running-shoes
              schema:
                $ref: '#/components/schemas/UrlDetailResponse'
          description: A single URL by id.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: URL not found
          description: URL not found in this project
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get URL
      tags:
      - URLs
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/urls/12\" \\\n  -H\
          \ \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/urls/12\"\
          , {\n  headers: { Authorization: \"Bearer zt_live_...\" }\n});\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/urls/12\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
    patch:
      description: 'Updates the URL string of an existing record in place, preserving
        the

        `id` and every prompt-to-URL association attached to it. Use this to

        correct typos or update a moved page without losing historical citation

        attribution.


        The `url` field is required and holds at most 1024 characters. The

        response is the record as stored after the update, read back, with its

        `prompt_count`. A URL another record of the project already holds,

        compared as the exact string, is 409 `URL_ALREADY_EXISTS` with that

        record''s `existing_id`; sending the record''s own URL changes nothing.


        Send an `Idempotency-Key` header to make the call safely retryable.

        Only a successful answer is stored and replayed; a refused request is

        not, so a retry with the same key runs it again.

        See [Idempotency](/content/guides/idempotency.html).


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `NOT_FOUND` | 404 | URL ID does not exist or belongs to a different project
        |

        | `URL_ALREADY_EXISTS` | 409 | Another URL of this project holds the new value.
        `existing_id` names it |

        | `IDEMPOTENCY_KEY_REUSED` | 409 | Same `Idempotency-Key` submitted with a
        different request body |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: update_url_v1_projects__project_id__urls__url_id__patch
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: Unique identifier of the URL.
        in: path
        name: url_id
        required: true
        schema:
          description: Unique identifier of the URL.
          maximum: 2147483647
          minimum: 1
          title: Url Id
          type: integer
      - description: 'Client-generated unique key that makes the request safe to retry.
          A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
          true`, to a retry with the same key and body; a refused request is not stored,
          so a retry runs it again. The same key with a different body is 409 `IDEMPOTENCY_KEY_REUSED`.'
        in: header
        name: Idempotency-Key
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: 'Client-generated unique key that makes the request safe to
            retry. A successful answer is stored for 24 hours and replayed, with `X-Idempotent-Replay:
            true`, to a retry with the same key and body; a refused request is not
            stored, so a retry runs it again. The same key with a different body is
            409 `IDEMPOTENCY_KEY_REUSED`.'
          title: Idempotency-Key
      requestBody:
        content:
          application/json:
            example:
              url: https://example.com/guides/best-running-shoes
            schema:
              $ref: '#/components/schemas/PatchUrlBody'
        required: true
      responses:
        '200':
          content:
            application/json:
              example:
                id: 12
                prompt_count: 8
                url: https://example.com/guides/best-running-shoes
              schema:
                $ref: '#/components/schemas/UrlUpdateResponse'
          description: The updated URL.
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: URL not found
          description: URL not found in this project
        '409':
          content:
            application/json:
              examples:
                idempotency_key_reused:
                  summary: Same Idempotency-Key with different body
                  value:
                    code: IDEMPOTENCY_KEY_REUSED
                    error: The provided Idempotency-Key has already been used with
                      a different request body.
                url_already_exists:
                  summary: Another URL of the project already holds this value
                  value:
                    code: URL_ALREADY_EXISTS
                    error: A URL with this value already exists
                    existing_id: 13
          description: Conflict - another URL holds the value, or idempotency-key
            reuse
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Update URL
      tags:
      - URLs
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl -X PATCH \"https://api.ziptie.ai/v1/projects/abc123/urls/12\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\" \\\n  -H \"Content-Type:\
          \ application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\"\
          url\":\"https://example.com/guides/best-running-shoes\"}'\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\"https://api.ziptie.ai/v1/projects/abc123/urls/12\"\
          , {\n  method: \"PATCH\",\n  headers: {\n    Authorization: \"Bearer zt_live_...\"\
          ,\n    \"Content-Type\": \"application/json\",\n    \"Idempotency-Key\"\
          : crypto.randomUUID(),\n  },\n  body: JSON.stringify({ url: \"https://example.com/guides/best-running-shoes\"\
          \ }),\n});\nconst data = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests, uuid\nres = requests.patch(\n    \"https://api.ziptie.ai/v1/projects/abc123/urls/12\"\
          ,\n    headers={\n        \"Authorization\": \"Bearer zt_live_...\",\n \
          \       \"Idempotency-Key\": str(uuid.uuid4()),\n    },\n    json={\"url\"\
          : \"https://example.com/guides/best-running-shoes\"},\n)\ndata = res.json()\n"
      x-required-scope: write
  /v1/projects/{project_id}/website/pages:
    get:
      description: 'Lists the pages of the project''s website that ziptie.ai has data
        for:

        how often AI engines cite each page, joined with the page''s Search

        Console impressions and clicks. A page is a normalised address, so

        spellings that differ only by scheme, `www.`, query string, fragment

        or a trailing slash are one page (path case is kept, so `/Blog` and

        `/blog` are two). The same figures drive the Website performance page

        in the app, so the two agree for the same filters.


        Citation figures follow `engines` (the requested engines the plan

        includes, or the plan''s engines when omitted) and are reported in

        `applied.engines`. Search Console figures are not split by engine and

        do not change with `engines`. They cover the rolling `window_days`

        window (7, 30 or 90 days ending at the latest check), all countries.

        Any other value, 28 included, answers 422 `VALIDATION_ERROR`.

        `applied.date_from` and `applied.date_to` are null: the window follows

        the checks, not the calendar, so `date_from`, `date_to`, `tag_ids` and

        `url_ids` are refused with 422.


        Each page names where its Search Console figures come from in

        `search_console`: `state` is `ok`, `no_data` (the domain is connected

        and Search Console holds nothing for the page) or `not_connected`, and

        `property` is the property string that reports the page''s domain, or

        null. One property owns each domain, so a page never mixes figures from

        two properties. A page without figures has null `impressions_window`

        and `clicks_window`, never zero. `applied.search_console_properties`

        counts the properties connected to the project and

        `applied.search_console_states` counts the matching pages by state.


        `scope` keeps all pages, only `added` URLs, or only pages with a

        connected `content` piece; `seen_in` keeps pages cited by an engine,

        pages with Search Console figures, or either. `folder` takes a

        `folder_key` from a row. Sort with `sort=field:asc|desc` on

        `prompts_citing_latest` (the default, descending), `citations_window`,

        `impressions_window`, `clicks_window` or `first_cited_day`; on the two

        Search Console keys a page without a figure sorts after every page

        with one. Pages are paged with `limit` (1-100) and `offset`. See

        [Pagination](/content/guides/pagination.html) and

        [Filtering and sorting](/content/guides/filtering-and-sorting.html).


        Website pages are part of paid plans. A workspace on a trial has no

        API access, so it gets `403 API_ADDON_INACTIVE` before this endpoint

        runs. `402 PAID_PLAN_REQUIRED` is the answer when the plan behind an

        allowed request cannot be confirmed as paid (the check fails closed). A

        workspace whose paid plan has lapsed keeps reading the history it

        already holds.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; website pages need a paid plan |

        | `ENGINE_NOT_ENABLED` | 400 | None of the requested `engines` is enabled
        for the workspace |

        | `VALIDATION_ERROR` | 422 | `window_days` is not 7, 30 or 90, `scope` or
        `seen_in` is not a listed value, `sort` is not a listed field, or a refused
        filter was sent |

        | `UPSTREAM_ERROR` | 500 | The project''s content connections could not be
        passed to the pages read; retrying will not help, contact support with the
        request ID |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: list_website_pages_v1_projects__project_id__website_pages_get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'Rolling window, in days ending at the latest check, for the
          `*_window` figures: 7, 30 or 90.'
        in: query
        name: window_days
        required: false
        schema:
          default: 30
          description: 'Rolling window, in days ending at the latest check, for the
            `*_window` figures: 7, 30 or 90.'
          title: Window Days
          type: integer
      - description: '`all` pages, only pages that are `added` URLs, or only pages
          with a connected `content` piece.'
        in: query
        name: scope
        required: false
        schema:
          default: all
          description: '`all` pages, only pages that are `added` URLs, or only pages
            with a connected `content` piece.'
          enum:
          - all
          - added
          - content
          title: Scope
          type: string
      - description: Keep pages cited by an engine (`citations`), pages with Search
          Console figures (`search_console`), or either (`any`).
        in: query
        name: seen_in
        required: false
        schema:
          default: any
          description: Keep pages cited by an engine (`citations`), pages with Search
            Console figures (`search_console`), or either (`any`).
          enum:
          - any
          - citations
          - search_console
          title: Seen In
          type: string
      - description: Only pages in this folder, as `folder_key` on a row.
        in: query
        name: folder
        required: false
        schema:
          anyOf:
          - maxLength: 512
            type: string
          - type: 'null'
          description: Only pages in this folder, as `folder_key` on a row.
          title: Folder
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      - description: Free-text filter. Matched against the resource's primary text
          field.
        in: query
        name: q
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Free-text filter. Matched against the resource's primary text
            field.
          title: Q
      - description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
          Allowed fields differ per resource.
        in: query
        name: sort
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Sort expression in 'field:direction' form (e.g. 'created_at:desc').
            Allowed fields differ per resource.
          title: Sort
      - description: Maximum number of items to return per page. Defaults to 50.
        in: query
        name: limit
        required: false
        schema:
          default: 50
          description: Maximum number of items to return per page. Defaults to 50.
          title: Limit
          type: integer
      - description: Zero-based offset into the result set for pagination, 0-2147483647.
        in: query
        name: offset
        required: false
        schema:
          default: 0
          description: Zero-based offset into the result set for pagination, 0-2147483647.
          title: Offset
          type: integer
      responses:
        '200':
          content:
            application/json:
              example:
                applied:
                  date_from: null
                  date_to: null
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                  scope: all
                  search_console_properties: 1
                  search_console_states:
                    no_data: 2
                    not_connected: 14
                    ok: 41
                  seen_in: any
                  window_days: 30
                data:
                - citations_window: 12
                  clicks_window: 412
                  domain: example.com
                  failed_checks_window: 1
                  first_cited_day: '2026-02-03'
                  folder_key: example.com/blog
                  has_content_piece: true
                  host: example.com
                  impressions_window: 8240
                  is_added: true
                  page_id: 9f2c1b7a4d6e8f031a5b7c9d2e4f60812a3b4c5d6e7f8091a2b3c4d5e6f70812
                  page_key: example.com/blog/best-running-shoes
                  prompts_citing_latest: 3
                  search_console:
                    property: sc-domain:example.com
                    state: ok
                  targeted_cited_latest: 2
                  targeted_cited_or_mentioned_latest: 2
                  targeted_mentioned_latest: 1
                  targeted_prompts: 4
                  url: https://example.com/blog/best-running-shoes
                  url_id: 12
                  variant_count: 2
                - citations_window: 2
                  clicks_window: null
                  domain: blog.example.org
                  failed_checks_window: 0
                  first_cited_day: '2026-04-02'
                  folder_key: blog.example.org/review
                  has_content_piece: false
                  host: blog.example.org
                  impressions_window: null
                  is_added: false
                  page_id: 1c8e3a5b7d9f0123456789abcdef0123456789abcdef0123456789abcdef0123
                  page_key: blog.example.org/review/trail-runners
                  prompts_citing_latest: 1
                  search_console:
                    property: null
                    state: not_connected
                  targeted_cited_latest: 0
                  targeted_cited_or_mentioned_latest: 0
                  targeted_mentioned_latest: 0
                  targeted_prompts: 0
                  url: https://blog.example.org/review/trail-runners
                  url_id: null
                  variant_count: 1
                limit: 50
                offset: 0
                total: 57
              schema:
                $ref: '#/components/schemas/WebsitePagesResponse'
          description: Paginated list of the project's website pages.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Website pages are part of paid plans. Subscribe to read them.
          description: The workspace's plan cannot be confirmed as paid; website pages
            need a paid plan
        '422':
          content:
            application/json:
              example:
                code: VALIDATION_ERROR
                error: window_days must be one of 7, 30, 90
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: A parameter is outside what the endpoint accepts
      summary: List website pages
      tags:
      - Website
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/website/pages?window_days=30&sort=citations_window:desc&limit=50\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/website/pages?window_days=30&sort=citations_window:desc&limit=50\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/website/pages\"\
          ,\n    params={\"window_days\": 30, \"sort\": \"citations_window:desc\"\
          , \"limit\": 50},\n    headers={\"Authorization\": \"Bearer zt_live_...\"\
          },\n)\ndata = res.json()\n"
      x-required-scope: read
  /v1/projects/{project_id}/website/pages/{page_id}:
    get:
      description: 'Returns one website page in full: its per-check series, the prompts

        linked to it as their target page, the prompts whose checks cited it,

        and, when a content piece on the page has a confirmed publish date,

        the comparison around that date. `page_id` is the `page_id` of a row

        from `GET /v1/projects/{project_id}/website/pages`.


        `series` has one point per check cycle: the window is the 90 days to

        the latest check unless you send `date_from` and `date_to`, in which

        case `applied` echoes them. Citation figures follow `engines` and

        `applied.engines` reports the engines read. `search_console` names the

        state of the page''s Search Console figures and the `property` behind

        them, as on the list.


        `before_after` is null until a content piece on the page has a

        confirmed publish date. The anchor is the latest confirmed date among

        the page''s pieces and `earlier_dates` lists the others. Each targeted

        prompt is judged on its own checks: its before side is its cycles

        before the anchor date (up to 90 days), its after side the cycles

        after it, and a cycle that starts on the anchor day is on neither

        side. A prompt linked after the anchor date has no before figure and

        reads `added_after_publishing`. On each prompt, `before` and `after`

        are flags, not counts: `cited` and `mentioned` say whether the

        prompt''s last cycle on that side cited the page and mentioned the

        brand, `cycles` counts its cycles there, and `after.cited_any` says

        whether any after cycle cited the page. The counts across prompts

        are in `before_after.citations`. `state` is `too_early` until at least

        3 checks and 14 days have passed after the anchor. Search Console

        figures are equal windows of whole days either side of the anchor,

        from the property that owns the page''s domain; when the held history

        does not cover the before window, `search_console.state` is

        `not_enough_history` (or `history_loading` while the history is still

        being loaded) and no before figure is shown. Figures are absolute

        counts, never percentages, and the block compares what was observed

        before and after a date; it does not measure cause and effect.


        `why` lists up to three reasons the figures look as they do, as codes

        with parameters (for example `too_early`, `not_cited`, `partly_cited`,

        `impressions_not_cited`, `not_in_search_console`, `no_before`,

        `not_enough_history`, `unchanged`, `not_enough_data`). Render your own

        sentence from the code and its `params`; the codes are the contract.


        Website pages are part of paid plans. A workspace on a trial has no

        API access, so it gets `403 API_ADDON_INACTIVE` before this endpoint

        runs. `402 PAID_PLAN_REQUIRED` is the answer when the plan behind an

        allowed request cannot be confirmed as paid (the check fails closed). A

        workspace whose paid plan has lapsed keeps reading the history it

        already holds.


        **Errors specific to this endpoint:**


        | Code | HTTP | When |

        |------|------|------|

        | `INVALID_ENGINE_NAME` | 400 | `engines` names an engine that does not exist
        |

        | `PAID_PLAN_REQUIRED` | 402 | The workspace''s plan cannot be confirmed as
        paid; website pages need a paid plan |

        | `NOT_FOUND` | 404 | The page id is not a page of this project |

        | `ENGINE_NOT_ENABLED` | 400 | None of the requested `engines` is enabled
        for the workspace |

        | `SERVICE_UNAVAILABLE` | 503 | The page''s content connections could not
        be read; retry |

        | `UPSTREAM_ERROR` | 500 | The page''s publish dates could not be passed to
        the page read; retrying will not help, contact support with the request ID
        |


        See [error codes](/content/error-codes.html) for the standard errors (401,
        402, 403, 422, 429, 500, 503).

        '
      operationId: get_website_page_v1_projects__project_id__website_pages__page_id__get
      parameters:
      - description: Unique identifier of the project.
        in: path
        name: project_id
        required: true
        schema:
          description: Unique identifier of the project.
          title: Project Id
          type: string
      - description: 'The page id (`page_id` on a pages row): a 64-character lower-case
          hex string.'
        in: path
        name: page_id
        required: true
        schema:
          description: 'The page id (`page_id` on a pages row): a 64-character lower-case
            hex string.'
          pattern: ^[0-9a-f]{64}$
          title: Page Id
          type: string
      - description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
          are refused). Defaults to 30 days before today (UTC), the same check days
          as the dashboard's 30-day view.
        in: query
        name: date_from
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive first day, `YYYY-MM-DD` (a UTC calendar day; datetimes
            are refused). Defaults to 30 days before today (UTC), the same check days
            as the dashboard's 30-day view.
          title: Date From
      - description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today; a
          later day reads as today.
        in: query
        name: date_to
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Inclusive last day, `YYYY-MM-DD` (UTC). Defaults to today;
            a later day reads as today.
          title: Date To
      - description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
          See Engine names guide.
        in: query
        name: engines
        required: false
        schema:
          anyOf:
          - type: string
          - type: 'null'
          description: Comma-separated list of engine keys (e.g. 'chatgpt,perplexity').
            See Engine names guide.
          title: Engines
      responses:
        '200':
          content:
            application/json:
              example:
                added_twice: false
                applied:
                  date_from: null
                  date_to: null
                  engines:
                  - chatgpt
                  - google_aio
                  - perplexity
                before_after:
                  anchor_date: '2026-03-20'
                  checks_after: 6
                  citations:
                    after:
                      cited: 2
                      cited_any: 3
                      cited_or_mentioned: 3
                      mentioned: 2
                      prompts: 4
                    before:
                      cited: 0
                      cited_or_mentioned: 1
                      mentioned: 1
                      prompts: 4
                    failed_only_prompts: 0
                  days_after: 33
                  earlier_dates:
                  - '2026-01-10'
                  range:
                    after: null
                    before: null
                    not_enough_data:
                      answers: 18
                      prompts: 6
                  search_console:
                    after:
                      clicks: 96
                      impressions: 1870
                    before:
                      clicks: 54
                      impressions: 1100
                    history_from: '2025-06-02'
                    property: sc-domain:example.com
                    state: compared
                    window_days: 33
                  state: compared
                citing_prompts:
                - engines_cited:
                  - perplexity
                  id: 12388
                  last_cited_day: '2026-04-15'
                  text: top trail runners for beginners
                domain: example.com
                first_cited_day: '2026-02-03'
                folder_key: example.com/blog
                host: example.com
                is_added: true
                last_cited_day: '2026-04-22'
                page_id: 9f2c1b7a4d6e8f031a5b7c9d2e4f60812a3b4c5d6e7f8091a2b3c4d5e6f70812
                page_key: example.com/blog/best-running-shoes
                prompts:
                - added_after_publishing: false
                  after:
                    cited: true
                    cited_any: true
                    cycles: 4
                    mentioned: true
                  before:
                    cited: false
                    cycles: 3
                    mentioned: false
                  brand_mentioned_latest: true
                  engines_cited:
                  - chatgpt
                  - google_aio
                  id: 12345
                  last_cited_day: '2026-04-22'
                  linked_at: '2026-03-02T10:00:00+00:00'
                  only_failed_latest: false
                  text: best running shoes for flat feet
                search_console:
                  property: sc-domain:example.com
                  state: ok
                series:
                - check_no: 14
                  cycle_day: '2026-04-15'
                  failed_prompts: 0
                  prompts_citing: 2
                  targeted_cited: 1
                  targeted_mentioned: 1
                - check_no: 15
                  cycle_day: '2026-04-22'
                  failed_prompts: 0
                  prompts_citing: 3
                  targeted_cited: 2
                  targeted_mentioned: 1
                url: https://example.com/blog/best-running-shoes
                url_id: 12
                variant_count: 2
                why:
                - code: partly_cited
                  params:
                    c: 2
                    k: 2
                    m: 1
                    n: 4
              schema:
                $ref: '#/components/schemas/WebsitePageDetail'
          description: One website page with its checks, prompts and publish-date
            comparison.
        '400':
          content:
            application/json:
              examples:
                engine_not_enabled:
                  summary: Every requested engine is off the workspace's plan
                  value:
                    code: ENGINE_NOT_ENABLED
                    error: None of the requested engines is enabled for this workspace
                invalid_engine_name:
                  summary: engines names an engine that does not exist
                  value:
                    code: INVALID_ENGINE_NAME
                    error: 'Unknown engine name: chatgtp'
          description: An engine name that does not exist, or no requested engine
            on the workspace's plan
        '402':
          content:
            application/json:
              example:
                code: PAID_PLAN_REQUIRED
                error: Website pages are part of paid plans. Subscribe to read them.
          description: The workspace's plan cannot be confirmed as paid; website pages
            need a paid plan
        '404':
          content:
            application/json:
              example:
                code: NOT_FOUND
                error: Page not found
          description: The page id is not a page of this project
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
          description: Validation Error
      summary: Get website page
      tags:
      - Website
      x-codeSamples:
      - label: curl
        lang: curl
        source: "curl \"https://api.ziptie.ai/v1/projects/abc123/website/pages/9f2c1b7a4d6e8f031a5b7c9d2e4f60812a3b4c5d6e7f8091a2b3c4d5e6f70812\"\
          \ \\\n  -H \"Authorization: Bearer zt_live_...\"\n"
      - label: Node
        lang: JavaScript
        source: "const res = await fetch(\n  \"https://api.ziptie.ai/v1/projects/abc123/website/pages/9f2c1b7a4d6e8f031a5b7c9d2e4f60812a3b4c5d6e7f8091a2b3c4d5e6f70812\"\
          ,\n  { headers: { Authorization: \"Bearer zt_live_...\" } }\n);\nconst data\
          \ = await res.json();\n"
      - label: Python
        lang: Python
        source: "import requests\nres = requests.get(\n    \"https://api.ziptie.ai/v1/projects/abc123/website/pages/9f2c1b7a4d6e8f031a5b7c9d2e4f60812a3b4c5d6e7f8091a2b3c4d5e6f70812\"\
          ,\n    headers={\"Authorization\": \"Bearer zt_live_...\"},\n)\ndata = res.json()\n"
      x-required-scope: read
security:
- ApiToken: []
servers:
- description: Production
  url: https://api.ziptie.ai
tags:
- description: 'The projects a token reaches, and for each one the configuration that

    shapes every other number on this surface - connected domains and

    brands, country, language, device, engine roster and check cadence -

    plus the check-freshness block. List them with `GET /v1/projects`, then

    read one with `GET /v1/projects/{project_id}`.

    '
  name: Projects
- description: 'Prompts you track in a project, plus the per-engine check history

    they generate. Each prompt is run on the project''s monitoring

    schedule against the configured AI engines; results land under

    `/checks`.


    A write here (create, edit or delete) lands in the app as the app''s

    own write does: its dashboards show the change on their next load,

    and an Action Center card the write completes is resolved.

    '
  name: Prompts
- description: 'Color-coded labels for organizing prompts and URLs.


    A write here (create, edit or delete) lands in the app as the app''s

    own write does: its dashboards show the change on their next load,

    and an Action Center card the write completes is resolved.

    '
  name: Tags
- description: 'Pages associated with the project for citation tracking.


    A write here (create, edit or delete) lands in the app as the app''s

    own write does: its dashboards show the change on their next load,

    and an Action Center card the write completes is resolved.

    '
  name: URLs
- description: 'Competitor citations, mentions, and trends derived from the

    project''s prompt checks across AI engines.

    '
  name: Competitors
- description: Labels you define per project to group the brands and domains you compete
    with. Separate from prompt tags.
  name: Competitor tags
- description: 'Project-level rollups for the main UI views: overview, LLM trend

    lines, and UGC impact.

    '
  name: Dashboard
- description: 'Google Search Console data for one of the project''s connected

    properties at a time - pages, queries, and trends. Without `property`

    the primary property (the earliest connected) is read.

    '
  name: Search Console
- description: 'The pages of the project''s website that ziptie.ai has data for: how

    often AI engines cite each page, joined with its Search Console

    impressions and clicks, and the comparison around a content piece''s

    confirmed publish date. Part of paid plans: a trial has no API

    access and gets `403 API_ADDON_INACTIVE`, and a request whose plan

    cannot be confirmed as paid gets `402 PAID_PLAN_REQUIRED`.

    '
  name: Website
- description: 'Brand facts you register about your own business, and the problems
    that

    open when AI answers contradict them. A model makes the judgement and can

    be wrong, so a problem can be dismissed and a fact edited. The surface

    returns counts and named states, never a rate or a score. Part of paid

    plans: a trial has no API access, and a request whose plan cannot be

    confirmed as paid gets `402 PAID_PLAN_REQUIRED`. Fact writes need the

    `admin` token scope and dismissals the `write` scope.

    '
  name: Answer accuracy
- description: Per-project API rate limits and current usage counters.
  name: Limits
- description: 'Topic clusters extracted from sources cited under the project''s

    monitored prompts, the sources backing each topic, and the AI-generated

    prompt suggestions derived from those clusters. Gated by

    `source_impact_enabled` on the workspace''s subscription.

    '
  name: UGC Impact
- description: 'Model Context Protocol endpoint. JSON-RPC 2.0 over HTTP. See the

    MCP guides at `/content/mcp/overview.html` for protocol details and

    client setup instructions. Each `tools/call` costs one rate-limit unit,

    the same as the `/v1/` request it runs; `initialize` and `tools/list`

    are not counted.

    '
  name: MCP
