{
  "openapi": "3.1.0",
  "info": {
    "title": "CiteHawk API",
    "version": "1.0.0",
    "description": "REST API for a CiteHawk workspace's AI-visibility (GEO) data. Every request is scoped to the single workspace that owns the API key in the Authorization header, so callers never supply a workspace id and cannot read across tenants. Use this API to pull tracked brands, the daily visibility metrics timeseries with latest KPIs, the workspace's headline Visibility and Brand Health scores with history, and the raw AI collection responses (with citations) that power those metrics. Agency-plan keys carrying the write:prospect-scans scope can additionally start and poll white-label prospect scans. Designed for ChatGPT custom GPT Actions, Looker Studio, and any non-MCP agent framework. An MCP server with richer analysis tools is also available at https://www.citehawk.com/api/mcp.",
    "contact": {
      "name": "CiteHawk",
      "url": "https://www.citehawk.com"
    },
    "x-logo": {
      "url": "https://www.citehawk.com/logo.png",
      "altText": "CiteHawk"
    }
  },
  "servers": [
    {
      "url": "https://www.citehawk.com/api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Brands",
      "description": "Tracked brands for the workspace."
    },
    {
      "name": "Metrics",
      "description": "Daily AI-visibility metrics and KPIs."
    },
    {
      "name": "Responses",
      "description": "Raw AI collection responses with citations."
    },
    {
      "name": "Scores",
      "description": "Headline Visibility / Brand Health scores and their history."
    },
    {
      "name": "Prospect Scans",
      "description": "Agency embed API: start and poll white-label prospect scans."
    },
    {
      "name": "Looker Studio",
      "description": "Backing endpoints for the CiteHawk Looker Studio community connector. These live at /api/looker-studio (not /api/v1), so each operation carries its own server URL. Both require an API key with the read:metrics scope."
    }
  ],
  "paths": {
    "/brands": {
      "get": {
        "operationId": "getBrands",
        "summary": "List tracked brands",
        "description": "Returns every brand tracked in the workspace associated with the API key. The primary (owned) brand is returned first, then competitors alphabetically.",
        "tags": [
          "Brands"
        ],
        "responses": {
          "200": {
            "description": "The workspace's brands.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BrandsResponse"
                },
                "example": {
                  "brands": [
                    {
                      "id": "a1b2c3d4-0000-4000-8000-000000000001",
                      "workspaceId": "58452a90-0000-4000-8000-000000000000",
                      "name": "Acme Analytics",
                      "domain": "acme.com",
                      "aliases": [
                        "Acme",
                        "Acme Inc"
                      ],
                      "relatedDomains": [
                        "acmeanalytics.com"
                      ],
                      "isPrimary": true,
                      "color": "#4F46E5",
                      "industry": "SaaS analytics",
                      "markets": [
                        "US",
                        "GB"
                      ],
                      "onboardingAnalysis": null,
                      "createdAt": "2026-05-01T09:00:00.000Z",
                      "updatedAt": "2026-06-30T09:00:00.000Z"
                    }
                  ],
                  "workspaceId": "58452a90-0000-4000-8000-000000000000"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or revoked API key"
                }
              }
            }
          }
        }
      }
    },
    "/metrics": {
      "get": {
        "operationId": "getMetrics",
        "summary": "Daily metrics timeseries and latest KPIs",
        "description": "Returns the daily AI-visibility metrics timeseries (per date, provider, and brand) plus the latest aggregated KPIs for the primary brand. sovPercentage is 0-100; mentionRate and citationRate are fractions 0-1; avgSentiment is roughly -1..1.",
        "tags": [
          "Metrics"
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Size of the timeseries window in days. Clamped to 1-365.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          },
          {
            "name": "promptType",
            "in": "query",
            "required": false,
            "description": "Metric basis. 'discovery' (default) counts unbranded discovery prompts; 'branded' counts brand-name prompts; 'all' mixes both. Discovery is the product default for like-for-like share of voice.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "discovery",
                "branded"
              ],
              "default": "discovery"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Timeseries plus latest KPIs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetricsResponse"
                },
                "example": {
                  "timeseries": [
                    {
                      "date": "2026-06-30",
                      "provider": "openai",
                      "brandId": "a1b2c3d4-0000-4000-8000-000000000001",
                      "brandName": "Acme Analytics",
                      "isPrimary": true,
                      "promptType": "discovery",
                      "sovPercentage": 18.4,
                      "mentionRate": 0.42,
                      "citationRate": 0.15,
                      "avgSentiment": 0.31,
                      "mentionCount": 21,
                      "citationCount": 8,
                      "totalResponses": 50
                    }
                  ],
                  "kpis": {
                    "date": "2026-06-30",
                    "sov": 18.4,
                    "mentionRate": 0.42,
                    "citationRate": 0.15,
                    "avgSentiment": 0.31,
                    "mentionCount": 21,
                    "citationCount": 8,
                    "totalResponses": 50,
                    "trend": {
                      "sov": 2.1,
                      "mentionRate": 0.04,
                      "citationRate": 0.01,
                      "avgSentiment": -0.02
                    }
                  },
                  "days": 30
                }
              }
            }
          },
          "400": {
            "description": "A query parameter was present but not numeric.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid 'days' parameter: expected a number, got 'abc'."
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/responses": {
      "get": {
        "operationId": "getResponses",
        "summary": "Recent AI collection responses",
        "description": "Returns the most recent raw AI answers collected for the workspace, newest first, each with its provider, model, full response text, extracted citations, cost, and timing. This is the underlying evidence behind the metrics.",
        "tags": [
          "Responses"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return. Capped at 200.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of rows to skip for pagination.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent responses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResponsesResponse"
                },
                "example": {
                  "responses": [
                    {
                      "id": "f0e1d2c3-0000-4000-8000-000000000001",
                      "provider": "openai",
                      "promptId": "b2c3d4e5-0000-4000-8000-000000000002",
                      "model": "gpt-4o",
                      "sampleIndex": 0,
                      "responseText": "For analytics, popular options include Acme Analytics, ...",
                      "citations": [
                        {
                          "url": "https://g2.com/categories/analytics",
                          "title": "Best Analytics Software"
                        }
                      ],
                      "error": null,
                      "costUsd": 0.0042,
                      "responseTimeMs": 3120,
                      "collectedAt": "2026-06-30T02:00:00.000Z"
                    }
                  ],
                  "limit": 50,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "description": "A query parameter was present but not numeric.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid 'limit' parameter: expected a number, got 'abc'."
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/scores": {
      "get": {
        "operationId": "getScores",
        "summary": "Latest Visibility / Brand Health scores and history",
        "description": "Returns the workspace's latest Visibility and Brand Health scores for the primary brand plus a per-collection-day history series (visibility, brandHealth, presence, authority, sentiment, accuracy) \u2014 the same canonical series the dashboard charts. When no scores exist yet, latest is null and history is empty.",
        "tags": [
          "Scores"
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Size of the history window in days. Clamped to 1-365.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 90
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Latest scores plus history.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoresResponse"
                },
                "example": {
                  "workspaceId": "58452a90-0000-4000-8000-000000000000",
                  "brandId": "a1b2c3d4-0000-4000-8000-000000000001",
                  "latest": {
                    "date": "2026-06-30",
                    "visibility": 62.5,
                    "brandHealth": 71.2
                  },
                  "history": [
                    {
                      "date": "2026-06-30",
                      "visibility": 62.5,
                      "brandHealth": 71.2,
                      "presence": 34.1,
                      "authority": 28.4,
                      "sentiment": 32.8,
                      "accuracy": 40.0
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A query parameter was present but not numeric.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid 'days' parameter: expected a number, got 'abc'."
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or revoked API key"
                }
              }
            }
          }
        }
      }
    },
    "/prospect-scans": {
      "post": {
        "operationId": "startProspectScan",
        "summary": "Start a white-label prospect scan",
        "description": "Starts a prospect-report scan headlessly from an agency's own site or app. Requires an API key with the explicit write:prospect-scans scope and an Agency plan with white-label enabled. Scans consume the same quota pool as the dashboard. A repeat request for the same target inside the 7-day dedupe window returns the existing scan with cached true and consumes nothing. If webhook_url is supplied, the webhook fires only for scans this request actually creates \u2014 cached responses carry the answer inline or are polled.",
        "tags": [
          "Prospect Scans"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProspectScanStartRequest"
              },
              "example": {
                "target_url": "prospect.com",
                "lead": {
                  "email": "jane@prospect.com",
                  "name": "Jane Doe"
                },
                "webhook_url": "https://agency.example/hooks/citehawk"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Dedupe cache hit: an existing scan for this target inside the 7-day window. No webhook will fire; poll GET /prospect-scans/{id} if it is still running.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProspectScanAccepted"
                },
                "example": {
                  "scan_id": "f0e1d2c3-0000-4000-8000-000000000009",
                  "status": "complete",
                  "cached": true,
                  "report_url": "https://agency.citehawk.com/prospect-report/abc123"
                }
              }
            }
          },
          "202": {
            "description": "Scan queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProspectScanAccepted"
                },
                "example": {
                  "scan_id": "f0e1d2c3-0000-4000-8000-000000000009",
                  "status": "queued",
                  "cached": false
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or revoked API key"
                }
              }
            }
          },
          "402": {
            "description": "Monthly included scans used and no credits remain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "API key lacks the write:prospect-scans scope, or the workspace is not on an Agency plan with white-label.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid request body (bad target_url, lead, or webhook_url).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Daily/concurrent scan cap reached, or this target is being scanned too frequently.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/prospect-scans/{id}": {
      "get": {
        "operationId": "getProspectScan",
        "summary": "Prospect scan status and results",
        "description": "Status plus results for a prospect scan in the key's workspace (scans started from the dashboard included). Rows are strictly workspace-scoped: an id minted under another workspace returns 404. Requires the same write:prospect-scans scope as the POST.",
        "tags": [
          "Prospect Scans"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The scan_id returned by POST /prospect-scans.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Scan status. report_url and summary appear when complete; error appears when failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProspectScanStatus"
                },
                "example": {
                  "scan_id": "f0e1d2c3-0000-4000-8000-000000000009",
                  "target_domain": "prospect.com",
                  "brand": "Prospect Inc",
                  "status": "complete",
                  "pct": 100,
                  "created_at": "2026-06-30T02:00:00.000Z",
                  "completed_at": "2026-06-30T02:06:00.000Z",
                  "report_url": "https://agency.citehawk.com/prospect-report/abc123",
                  "summary": {
                    "visibility_score": 41.0,
                    "engine_count": 8,
                    "category_rank": 3,
                    "geo_audit": {
                      "score": 62,
                      "grade": "C"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid or revoked API key"
                }
              }
            }
          },
          "404": {
            "description": "No scan with this id in the key's workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/looker-studio/schema": {
      "servers": [
        {
          "url": "https://www.citehawk.com/api"
        }
      ],
      "get": {
        "operationId": "getLookerStudioSchema",
        "summary": "Field schema for the Looker Studio connector",
        "description": "Returns the field descriptors the community connector turns into Looker Studio fields. Percent-semantic fields (sov, mention_rate, citation_rate) are 0-1 in the data endpoint. Called by the connector's getSchema().",
        "tags": [
          "Looker Studio"
        ],
        "responses": {
          "200": {
            "description": "The connector field schema.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookerStudioSchemaResponse"
                },
                "example": {
                  "schema": [
                    {
                      "name": "date",
                      "label": "Date",
                      "dataType": "STRING",
                      "semantics": {
                        "conceptType": "DIMENSION",
                        "semanticType": "YEAR_MONTH_DAY"
                      }
                    },
                    {
                      "name": "sov",
                      "label": "Share of voice",
                      "dataType": "NUMBER",
                      "semantics": {
                        "conceptType": "METRIC",
                        "semanticType": "PERCENT"
                      },
                      "aggregation": "AVG"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid, expired or revoked API key"
                }
              }
            }
          },
          "403": {
            "description": "The API key is valid but lacks the 'read:metrics' scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This API key does not have the 'read:metrics' scope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Rate limit exceeded"
                }
              }
            }
          }
        }
      }
    },
    "/looker-studio/data": {
      "servers": [
        {
          "url": "https://www.citehawk.com/api"
        }
      ],
      "get": {
        "operationId": "getLookerStudioData",
        "summary": "Daily metric rows for a date range",
        "description": "Returns one row per date, brand, AI model and prompt type for the workspace's global market, over the requested date range. The range is honored as given and capped at 366 days. Every percent-semantic field is a fraction 0-1: sov is the stored 0-100 share of voice divided by 100, mention_rate and citation_rate are stored 0-1 and pass through. Called by the connector's getData().",
        "tags": [
          "Looker Studio"
        ],
        "parameters": [
          {
            "name": "startDate",
            "in": "query",
            "required": true,
            "description": "First day of the range, inclusive.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-06-01"
            }
          },
          {
            "name": "endDate",
            "in": "query",
            "required": true,
            "description": "Last day of the range, inclusive. Must be on or after startDate. A range wider than 366 days is trimmed from the start.",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-06-30"
            }
          },
          {
            "name": "promptType",
            "in": "query",
            "required": false,
            "description": "Metric basis. 'discovery' (default) counts unbranded discovery prompts; 'branded' counts brand-name prompts; 'all' mixes both.",
            "schema": {
              "type": "string",
              "enum": [
                "discovery",
                "branded",
                "all"
              ],
              "default": "discovery"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Metric rows for the range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookerStudioDataResponse"
                },
                "example": {
                  "data": [
                    {
                      "date": "20260630",
                      "brand_name": "Acme Analytics",
                      "provider": "openai",
                      "prompt_type": "discovery",
                      "metric_version": "1",
                      "sov": 0.184,
                      "mention_rate": 0.42,
                      "citation_rate": 0.15,
                      "avg_sentiment": 0.31,
                      "responses_collected": 50
                    }
                  ],
                  "rows": 1
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed startDate or endDate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "startDate must be YYYY-MM-DD"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, expired or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Invalid, expired or revoked API key"
                }
              }
            }
          },
          "403": {
            "description": "The API key is valid but lacks the 'read:metrics' scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This API key does not have the 'read:metrics' scope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Rate limit exceeded"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Supply a CiteHawk API key (self-served from Settings) as: Authorization: Bearer <api_key>"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "Brand": {
        "type": "object",
        "description": "A tracked brand row. The primary brand (isPrimary true) is the workspace's own brand; the rest are competitors.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "workspaceId": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "aliases": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "relatedDomains": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "isPrimary": {
            "type": "boolean"
          },
          "color": {
            "type": [
              "string",
              "null"
            ]
          },
          "industry": {
            "type": [
              "string",
              "null"
            ]
          },
          "markets": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "onboardingAnalysis": {
            "type": [
              "object",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "workspaceId",
          "name",
          "domain",
          "isPrimary"
        ]
      },
      "BrandsResponse": {
        "type": "object",
        "properties": {
          "brands": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Brand"
            }
          },
          "workspaceId": {
            "type": "string",
            "format": "uuid"
          }
        },
        "required": [
          "brands",
          "workspaceId"
        ]
      },
      "MetricRow": {
        "type": "object",
        "description": "One (date, provider, brand) metric row. provider may be a specific AI platform (e.g. 'openai') in the timeseries.",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "provider": {
            "type": "string"
          },
          "brandId": {
            "type": "string",
            "format": "uuid"
          },
          "brandName": {
            "type": "string"
          },
          "isPrimary": {
            "type": "boolean"
          },
          "promptType": {
            "type": "string",
            "enum": [
              "all",
              "discovery",
              "branded"
            ]
          },
          "sovPercentage": {
            "type": [
              "number",
              "null"
            ],
            "description": "Share of voice, 0-100."
          },
          "mentionRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Fraction 0-1."
          },
          "citationRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Fraction 0-1."
          },
          "avgSentiment": {
            "type": [
              "number",
              "null"
            ],
            "description": "Roughly -1..1."
          },
          "mentionCount": {
            "type": [
              "integer",
              "null"
            ]
          },
          "citationCount": {
            "type": [
              "integer",
              "null"
            ]
          },
          "totalResponses": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Kpis": {
        "type": [
          "object",
          "null"
        ],
        "description": "Latest aggregated KPIs for the primary brand (provider 'all'), with trend versus 7 days earlier. Null when no analysed metrics exist yet.",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "sov": {
            "type": [
              "number",
              "null"
            ]
          },
          "mentionRate": {
            "type": [
              "number",
              "null"
            ]
          },
          "citationRate": {
            "type": [
              "number",
              "null"
            ]
          },
          "avgSentiment": {
            "type": [
              "number",
              "null"
            ]
          },
          "mentionCount": {
            "type": [
              "integer",
              "null"
            ]
          },
          "citationCount": {
            "type": [
              "integer",
              "null"
            ]
          },
          "totalResponses": {
            "type": [
              "integer",
              "null"
            ]
          },
          "trend": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "sov": {
                "type": "number"
              },
              "mentionRate": {
                "type": "number"
              },
              "citationRate": {
                "type": "number"
              },
              "avgSentiment": {
                "type": "number"
              }
            }
          }
        }
      },
      "MetricsResponse": {
        "type": "object",
        "properties": {
          "timeseries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MetricRow"
            }
          },
          "kpis": {
            "$ref": "#/components/schemas/Kpis"
          },
          "days": {
            "type": "integer"
          }
        },
        "required": [
          "timeseries",
          "days"
        ]
      },
      "ResponseRow": {
        "type": "object",
        "description": "One raw AI collection response.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "provider": {
            "type": "string"
          },
          "promptId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          },
          "sampleIndex": {
            "type": [
              "integer",
              "null"
            ]
          },
          "responseText": {
            "type": [
              "string",
              "null"
            ]
          },
          "citations": {
            "type": [
              "array",
              "null"
            ],
            "description": "Extracted citations (jsonb). Shape is provider-dependent but typically objects with url and title.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "costUsd": {
            "type": [
              "number",
              "null"
            ]
          },
          "responseTimeMs": {
            "type": [
              "integer",
              "null"
            ]
          },
          "collectedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ResponsesResponse": {
        "type": "object",
        "properties": {
          "responses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResponseRow"
            }
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          }
        },
        "required": [
          "responses",
          "limit",
          "offset"
        ]
      },
      "ScoreTrendPoint": {
        "type": "object",
        "description": "One collection day of scores. All scores are 0-100; null when that day lacks the inputs.",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "visibility": {
            "type": [
              "number",
              "null"
            ]
          },
          "brandHealth": {
            "type": [
              "number",
              "null"
            ]
          },
          "presence": {
            "type": [
              "number",
              "null"
            ]
          },
          "authority": {
            "type": [
              "number",
              "null"
            ]
          },
          "sentiment": {
            "type": [
              "number",
              "null"
            ]
          },
          "accuracy": {
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "date"
        ]
      },
      "ScoresResponse": {
        "type": "object",
        "properties": {
          "workspaceId": {
            "type": "string",
            "format": "uuid"
          },
          "brandId": {
            "type": "string",
            "format": "uuid",
            "description": "Primary brand id. Absent when the workspace has no scores yet."
          },
          "latest": {
            "type": [
              "object",
              "null"
            ],
            "description": "Latest scores for the primary brand; null when none exist yet.",
            "properties": {
              "date": {
                "type": "string",
                "format": "date"
              },
              "visibility": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "brandHealth": {
                "type": [
                  "number",
                  "null"
                ]
              }
            }
          },
          "history": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScoreTrendPoint"
            }
          }
        },
        "required": [
          "workspaceId",
          "latest",
          "history"
        ]
      },
      "ProspectScanStartRequest": {
        "type": "object",
        "properties": {
          "target_url": {
            "type": "string",
            "description": "Domain or URL of the prospect site to scan (e.g. prospect.com).",
            "minLength": 2,
            "maxLength": 2048
          },
          "lead": {
            "type": "object",
            "description": "Optional lead to associate with the scan.",
            "properties": {
              "email": {
                "type": "string",
                "format": "email",
                "maxLength": 254
              },
              "name": {
                "type": "string",
                "maxLength": 120
              }
            }
          },
          "webhook_url": {
            "type": "string",
            "description": "Public https URL to notify when a newly created scan completes. No IPs or local hostnames.",
            "maxLength": 2000
          }
        },
        "required": [
          "target_url"
        ]
      },
      "ProspectScanAccepted": {
        "type": "object",
        "properties": {
          "scan_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "complete",
              "failed"
            ]
          },
          "cached": {
            "type": "boolean",
            "description": "True when this request hit the 7-day dedupe window and returned an existing scan."
          },
          "report_url": {
            "type": "string",
            "description": "White-label report URL. Present only on cached responses whose scan is already complete."
          }
        },
        "required": [
          "scan_id",
          "status",
          "cached"
        ]
      },
      "ProspectScanSummary": {
        "type": "object",
        "description": "Headline results extracted from the finished report. Fields degrade to absent when the snapshot lacks them.",
        "properties": {
          "visibility_score": {
            "type": "number",
            "description": "Headline AI Visibility score, 0-100."
          },
          "engine_count": {
            "type": "integer"
          },
          "category_rank": {
            "type": "integer",
            "description": "1-based rank among tracked competitors (0 = unranked)."
          },
          "mention_rate_pct": {
            "type": "number"
          },
          "battleground": {
            "type": "object",
            "additionalProperties": true
          },
          "geo_audit": {
            "type": "object",
            "properties": {
              "score": {
                "type": "number"
              },
              "grade": {
                "type": "string"
              }
            }
          }
        }
      },
      "ProspectScanStatus": {
        "type": "object",
        "properties": {
          "scan_id": {
            "type": "string",
            "format": "uuid"
          },
          "target_domain": {
            "type": "string"
          },
          "brand": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "complete",
              "failed"
            ]
          },
          "pct": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Coarse progress."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "report_url": {
            "type": "string",
            "description": "Present when status is complete."
          },
          "summary": {
            "$ref": "#/components/schemas/ProspectScanSummary"
          },
          "error": {
            "type": "string",
            "description": "Present when status is failed."
          }
        },
        "required": [
          "scan_id",
          "target_domain",
          "status",
          "pct",
          "created_at"
        ]
      },
      "LookerStudioField": {
        "type": "object",
        "description": "One field descriptor. The connector maps conceptType and semanticType onto Looker Studio field types.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Field id, matching a key in a data row."
          },
          "label": {
            "type": "string",
            "description": "Human label shown in Looker Studio."
          },
          "dataType": {
            "type": "string",
            "enum": [
              "STRING",
              "NUMBER"
            ]
          },
          "semantics": {
            "type": "object",
            "properties": {
              "conceptType": {
                "type": "string",
                "enum": [
                  "DIMENSION",
                  "METRIC"
                ]
              },
              "semanticType": {
                "type": "string",
                "enum": [
                  "YEAR_MONTH_DAY",
                  "PERCENT",
                  "NUMBER"
                ],
                "description": "Absent means plain text for a dimension and plain number for a metric. PERCENT fields are fractions 0-1."
              }
            },
            "required": [
              "conceptType"
            ]
          },
          "aggregation": {
            "type": "string",
            "enum": [
              "AVG",
              "SUM"
            ],
            "description": "Default aggregation for a metric. Rates average; counts sum."
          }
        },
        "required": [
          "name",
          "label",
          "dataType",
          "semantics"
        ]
      },
      "LookerStudioSchemaResponse": {
        "type": "object",
        "properties": {
          "schema": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LookerStudioField"
            }
          }
        },
        "required": [
          "schema"
        ]
      },
      "LookerStudioRow": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "Collection day as YYYYMMDD, the format Looker Studio expects for a YEAR_MONTH_DAY dimension."
          },
          "brand_name": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "description": "The AI model the row is measured on. 'all' is the cross-model rollup."
          },
          "prompt_type": {
            "type": "string",
            "enum": [
              "discovery",
              "branded",
              "all"
            ]
          },
          "metric_version": {
            "type": "string",
            "description": "Which generation of the metric definitions wrote the row, as a string so it can be used as a dimension."
          },
          "sov": {
            "type": "number",
            "description": "Share of voice as a fraction 0-1."
          },
          "mention_rate": {
            "type": "number",
            "description": "Share of answers mentioning the brand, 0-1."
          },
          "citation_rate": {
            "type": "number",
            "description": "Share of answers citing the brand's own site, 0-1."
          },
          "avg_sentiment": {
            "type": "number",
            "description": "Average sentiment of the mentions, roughly -1..1."
          },
          "responses_collected": {
            "type": "integer"
          }
        },
        "required": [
          "date",
          "brand_name",
          "provider",
          "prompt_type",
          "metric_version",
          "sov",
          "mention_rate",
          "citation_rate",
          "avg_sentiment",
          "responses_collected"
        ]
      },
      "LookerStudioDataResponse": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LookerStudioRow"
            }
          },
          "rows": {
            "type": "integer",
            "description": "Number of rows in data."
          }
        },
        "required": [
          "data",
          "rows"
        ]
      }
    }
  }
}
