{
  "openapi": "3.1.0",
  "info": {
    "title": "FixAEO Customer API",
    "version": "2026.05.23",
    "description": "Bearer-authed read API over your FixAEO workspace. Create a key in Settings, then send it as `Authorization: Bearer <key>`. Every route is scoped to the key's own account.\n\nGenerated from the server's OpenAPI spec \u2014 see scripts/build-public-openapi.py."
  },
  "servers": [
    {
      "url": "https://api.fixaeo.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/public/v1/brands": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "List brands",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Brand list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brands": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Brand"
                      }
                    },
                    "count": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "description": "List brands (bearer-authed)"
      }
    },
    "/api/public/v1/brands/{slug}": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "Get a brand",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BrandSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Brand record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Brand"
                }
              }
            }
          }
        },
        "description": "Get a brand (bearer-authed)"
      }
    },
    "/api/public/v1/brands/{slug}/snapshots": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "Daily snapshot series",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BrandSlug"
          },
          {
            "$ref": "#/components/parameters/RangeQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Snapshot series.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brand_slug": {
                      "type": "string"
                    },
                    "range": {
                      "type": "string"
                    },
                    "snapshots": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Snapshot"
                      }
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Daily snapshot series (bearer-authed)"
      }
    },
    "/api/public/v1/brands/{slug}/mentions": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "Recent mentions",
        "description": "Region-scoped to the brand's primary active region by default; pass region/language to override.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BrandSlug"
          },
          {
            "name": "sentiment",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "",
                "pos",
                "neu",
                "neg"
              ]
            }
          },
          {
            "name": "region",
            "in": "query",
            "description": "ISO-3166-1 alpha-2 region filter; defaults to the brand's primary active region.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "language",
            "in": "query",
            "description": "ISO-639-1 language filter; defaults to the primary region's language.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Mention list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brand_slug": {
                      "type": "string"
                    },
                    "mentions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Mention"
                      }
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/brands/{slug}/prompts": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "List monitored prompts",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BrandSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Prompts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "prompts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Prompt"
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "Active prompts on THIS brand."
                    },
                    "used_total": {
                      "type": "integer",
                      "description": "Active prompts across ALL the user's brands \u2014 the shared pool the plan limit is enforced against.\n"
                    },
                    "limit": {
                      "type": "integer",
                      "description": "Plan prompt limit (shared across all brands)."
                    }
                  }
                }
              }
            }
          }
        },
        "description": "List monitored prompts (bearer-authed)"
      }
    },
    "/api/public/v1/portfolio": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "Portfolio rollup",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RangeQuery"
          },
          {
            "name": "region",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "us"
            }
          },
          {
            "name": "language",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Portfolio data.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Portfolio"
                }
              }
            }
          }
        },
        "description": "Portfolio rollup (bearer-authed)"
      }
    },
    "/api/public/v1/citations": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "Citation aggregation",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RangeQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Citation aggregation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Citations"
                }
              }
            }
          }
        },
        "description": "Citation aggregation (bearer-authed)"
      }
    },
    "/api/public/v1/citations/fanouts": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "Query fanouts",
        "description": "Bearer-authed twin of `/api/v1/me/citations/fanouts`. Returns the LLM-decomposed sub-questions per (tracked prompt x engine) for the calling key's workspace. `truncated_groups` is true when the response hit the group cap \u2014 narrow the range to see the remainder.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RangeQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Fanout groups within the range.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "range_days": {
                      "type": "integer"
                    },
                    "prompts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FanoutPromptGroup"
                      }
                    },
                    "truncated_groups": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/public/v1/topics": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "Workspace topic themes",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Themes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "themes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "theme": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          },
                          "tier": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "total_keywords": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Workspace topic themes (bearer-authed)"
      }
    },
    "/api/public/v1/model": {
      "get": {
        "tags": [
          "Public API"
        ],
        "summary": "Per-engine performance",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "brand_slug",
            "in": "query",
            "description": "Which brand's engine breakdown to return. Defaults to the newest tracked brand; pass a slug to select a specific one.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Engine breakdown.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "engines": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "engine": {
                            "type": "string"
                          },
                          "score": {
                            "type": "integer"
                          },
                          "delta": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "range": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Per-engine performance (bearer-authed)"
      }
    },
    "/api/public/v1/brands/{slug}/attribution": {
      "get": {
        "tags": [
          "Public API",
          "GA4"
        ],
        "summary": "Per-brand attribution",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BrandSlug"
          },
          {
            "$ref": "#/components/parameters/RangeQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Attribution metrics."
          }
        },
        "description": "Per-brand attribution (bearer-authed)"
      }
    },
    "/api/public/v1/brands/{slug}/search-performance": {
      "get": {
        "tags": [
          "Public API",
          "GSC"
        ],
        "summary": "Per-brand organic search performance",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BrandSlug"
          },
          {
            "$ref": "#/components/parameters/RangeQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Search performance metrics."
          },
          "403": {
            "description": "Paid plan required."
          }
        },
        "description": "Per-brand organic search performance (bearer-authed)"
      }
    },
    "/api/public/v1/brands/{slug}/agents": {
      "get": {
        "tags": [
          "Public API",
          "Agents"
        ],
        "summary": "Agent dashboard",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BrandSlug"
          },
          {
            "$ref": "#/components/parameters/RangeQuery"
          },
          {
            "$ref": "#/components/parameters/AgentBotQuery"
          },
          {
            "$ref": "#/components/parameters/AgentVendorQuery"
          },
          {
            "$ref": "#/components/parameters/AgentKindQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Daily hit metrics + tag snippet."
          }
        },
        "description": "Agent dashboard (bearer-authed)"
      }
    },
    "/api/public/v1/brands/{slug}/agents/pages": {
      "get": {
        "tags": [
          "Public API",
          "Agents"
        ],
        "summary": "Crawled-pages list / crawler logs",
        "description": "The complete per-URL list of URLs AI bots fetched \u2014 the public twin of GET /api/v1/me/brands/{slug}/agents/pages. EVERY crawled URL is included: static assets (`/_next/...`, images, CSS) and crawler-directive files (`/robots.txt`, `/llms.txt`) are reported alongside content pages, because what a crawler spends its budget on is the point of the report. Paginated, searchable, and filterable by status (`ok`, `error`, or one exact code such as `404`), bot, vendor, kind, and folder. Crawler filters are served from the (path x bot) rollup, which starts at the raw-hit retention horizon and grows from there \u2014 see retention_limited / coverage_from.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/BrandSlug"
          },
          {
            "$ref": "#/components/parameters/RangeQuery"
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/AgentBotQuery"
          },
          {
            "$ref": "#/components/parameters/AgentVendorQuery"
          },
          {
            "$ref": "#/components/parameters/AgentKindQuery"
          },
          {
            "name": "folder",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Top-level folder to restrict to, e.g. `blogs` for `/blogs/*`, or `/` for pages at the site root.\n"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "hits",
                "path",
                "errors",
                "uniques"
              ]
            },
            "description": "Column to order by. Defaults to `hits`. Unrecognised values fall back to the default rather than erroring.\n"
          },
          {
            "name": "dir",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            },
            "description": "Sort direction. Only honoured alongside `sort`. Defaults per column: descending for the numeric columns, ascending for `path`.\n"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10,
              "maximum": 500
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of crawled content pages + the total distinct count."
          }
        }
      }
    },
    "/api/public/v1/agents/ingest/vercel": {
      "get": {
        "tags": [
          "Public API",
          "Agents"
        ],
        "summary": "Vercel Drain endpoint verification probe",
        "description": "Answers Vercel's endpoint-verification probe with 200 and echoes any `verify` query value back as an `x-vercel-verify` response header. Deliberately UNAUTHENTICATED: the probe runs before the drain exists and Vercel documents neither its method nor whether it carries the drain's custom headers, so a 401 here would block drain creation. It returns no data and echoes only a value the caller placed in the URL. HEAD behaves identically.",
        "security": [],
        "parameters": [
          {
            "name": "verify",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Vercel endpoint-verification code, echoed back as `x-vercel-verify`."
          }
        ],
        "responses": {
          "200": {
            "description": "Always. Empty body."
          }
        }
      },
      "post": {
        "tags": [
          "Public API",
          "Agents"
        ],
        "summary": "Ingest a Vercel Drain delivery",
        "description": "Receives a Vercel Drain (formerly \"Log Drain\") delivery in VERCEL'S OWN format, so a Vercel-hosted customer needs no code: they paste this URL into a drain and add one `Authorization: Bearer <api-key>` custom header. We translate, classify the User-Agent through the same catalog the JS beacon uses, and store recognised AI-crawler requests with `channel=vercel`.\n\nAccepts both delivery encodings \u2014 a JSON array or NDJSON \u2014 sniffed from the body rather than from Content-Type, which Vercel does not guarantee; gzip is detected by magic bytes. Records without a `proxy` object (build output, function stdout, and the sample events Vercel posts when the drain is created) are counted as skipped, not errors.\n\nDELIBERATELY FORGIVING: a delivery we cannot parse, or one we fail to persist, still returns 200. Vercel publishes no retry policy and flags a drain (emailing the customer that the endpoint is broken) past 80% failed deliveries. Only a bad token, a foreign site_id, a signature mismatch or a spent quota return a non-2xx.\n\nRequires the Growth plan or higher and is metered against the same monthly server-log event cap as `/agents/ingest`.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "site_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The brand's public site id (the same value used by the JS tag). Ownership-checked against the API key's account. May also be sent as an `X-FixAEO-Site` header."
          },
          {
            "name": "verify",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Echoed back as `x-vercel-verify` on every response."
          },
          {
            "name": "x-vercel-signature",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Hex HMAC-SHA1 of the raw body using the drain's signing secret. Vercel makes the secret optional; verification is enforced only when a secret has been saved for the brand, and then a missing or mismatched signature is a 403."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/VercelLogRecord"
                }
              }
            },
            "application/x-ndjson": {
              "schema": {
                "type": "string",
                "description": "One JSON log record per line."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivery handled. Also returned for unparseable bodies and for deliveries containing no request records.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accepted": {
                      "type": "integer",
                      "description": "Crawler hits stored"
                    },
                    "scanned": {
                      "type": "integer",
                      "description": "Records seen in the delivery"
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Records that were not AI-crawler requests"
                    },
                    "monthly_used": {
                      "type": "integer"
                    },
                    "monthly_cap": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing site_id."
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "403": {
            "description": "Site not owned by this account, signature mismatch, or the plan is not entitled to server-log ingestion."
          },
          "404": {
            "description": "Unknown site_id."
          },
          "429": {
            "description": "Monthly server-log event cap reached."
          }
        }
      }
    },
    "/api/public/v1/agents/ingest": {
      "post": {
        "tags": [
          "Public API",
          "Agents"
        ],
        "summary": "Ingest server-log AI-crawler hits",
        "description": "Server-log ingestion for Agent Analytics. The customer's edge \u2014 a Cloudflare Worker, a generic CDN webhook, or a CSV log upload \u2014 forwards a batch of raw request records. Each User-Agent is classified with the same engine catalog the JS beacon uses and written to the same Agents dashboard, so server-side crawlers (GPTBot, ClaudeBot, etc.) that never execute JavaScript are captured. Paid plans only. The batch's site_id must belong to the authenticated account. Max 2000 records per batch; larger batches return 413.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "site_id",
                  "records"
                ],
                "properties": {
                  "site_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The site_id from the brand's Agents tab."
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "cf_worker",
                      "webhook",
                      "csv",
                      "wp_plugin"
                    ],
                    "default": "cf_worker",
                    "description": "Ingestion path. Unknown values coerce to cf_worker."
                  },
                  "records": {
                    "type": "array",
                    "maxItems": 2000,
                    "items": {
                      "type": "object",
                      "properties": {
                        "ua": {
                          "type": "string",
                          "description": "Request User-Agent (classification signal)."
                        },
                        "path": {
                          "type": "string",
                          "description": "Request path."
                        },
                        "ref": {
                          "type": "string",
                          "description": "Request Referer",
                          "if any.": null
                        },
                        "ip": {
                          "type": "string",
                          "description": "Client IP; hashed with the salt",
                          "never stored raw.": null
                        },
                        "ts": {
                          "type": "integer",
                          "format": "int64",
                          "description": "Unix millis when the hit occurred; 0/absent = now."
                        },
                        "status": {
                          "type": "integer",
                          "description": "HTTP status served (100\u2013599; out-of-range/absent stored as NULL). Powers the per-URL status column + failure-rate KPI."
                        },
                        "method": {
                          "type": "string",
                          "description": "HTTP request method (GET/POST/\u2026). Optional; absent stored as NULL."
                        },
                        "country": {
                          "type": "string",
                          "description": "ISO-3166 alpha-2 country code from the edge. Optional; must be 2 letters or it's dropped. Powers the by_country breakdown."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "accepted": {
                      "type": "integer",
                      "description": "Rows written this batch."
                    },
                    "monthly_used": {
                      "type": "integer",
                      "description": "Server-log events used by this account this calendar month (incl. this batch)."
                    },
                    "monthly_cap": {
                      "type": "integer",
                      "description": "The account's monthly server-log event cap for its plan."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing site_id or records, or invalid JSON."
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "403": {
            "description": "Plan not entitled (server-log ingestion requires Growth or higher), or the site_id does not belong to this account."
          },
          "404": {
            "description": "Unknown site_id."
          },
          "413": {
            "description": "Batch exceeds 2000 records; split it."
          },
          "429": {
            "description": "Monthly server-log event cap reached for this account's plan."
          }
        }
      }
    },
    "/api/public/v1/industry/metrics": {
      "get": {
        "tags": [
          "Public API",
          "Industry"
        ],
        "summary": "Comprehensive industry metrics",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RangeQuery"
          },
          {
            "$ref": "#/components/parameters/EngineQuery"
          },
          {
            "name": "region",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "us"
            }
          },
          {
            "name": "language",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "en"
            }
          },
          {
            "name": "brand_slug",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Defaults to the primary brand."
          }
        ],
        "responses": {
          "200": {
            "description": "Industry metrics payload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brand_slug": {
                      "type": "string"
                    },
                    "brand_name": {
                      "type": "string"
                    },
                    "range": {
                      "type": "string"
                    },
                    "from": {
                      "type": "string",
                      "format": "date"
                    },
                    "to": {
                      "type": "string",
                      "format": "date"
                    },
                    "engine": {
                      "type": "string"
                    },
                    "region": {
                      "type": "string"
                    },
                    "language": {
                      "type": "string"
                    },
                    "last_refreshed": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "ranking": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "mentions": {
                            "type": "integer"
                          },
                          "share": {
                            "type": "number"
                          },
                          "delta": {
                            "type": "number"
                          },
                          "is_own": {
                            "type": "boolean"
                          },
                          "is_pinned": {
                            "type": "boolean"
                          },
                          "domain": {
                            "type": "string",
                            "description": "Resolved competitor domain for the favicon; omitted when unknown"
                          },
                          "sentiment": {
                            "type": "object",
                            "properties": {
                              "pos": {
                                "type": "integer"
                              },
                              "neu": {
                                "type": "integer"
                              },
                              "neg": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    },
                    "ranking_count": {
                      "type": "integer"
                    },
                    "time_series": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "brand_mentions": {
                            "type": "integer"
                          },
                          "total_responses": {
                            "type": "integer"
                          },
                          "per_competitor": {
                            "type": "object",
                            "additionalProperties": {
                              "type": "integer"
                            }
                          }
                        }
                      }
                    },
                    "time_series_count": {
                      "type": "integer"
                    },
                    "own": {
                      "type": "object",
                      "properties": {
                        "total_mentions": {
                          "type": "integer"
                        },
                        "total_responses": {
                          "type": "integer"
                        },
                        "avg_position": {
                          "type": "number"
                        }
                      }
                    },
                    "tracked_competitors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "methodology": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "Comprehensive industry metrics (bearer-authed)"
      }
    },
    "/api/public/v1/industry/ranking": {
      "get": {
        "tags": [
          "Public API",
          "Industry"
        ],
        "summary": "Topic-filtered ranking",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "topic",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/RangeQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Topic-filtered ranking."
          }
        },
        "description": "Topic-filtered ranking (bearer-authed)"
      }
    },
    "/api/public/v1/industry/gap": {
      "get": {
        "tags": [
          "Public API",
          "Industry"
        ],
        "summary": "Gap analysis",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/RangeQuery"
          },
          {
            "name": "region",
            "in": "query",
            "description": "ISO-3166-1 alpha-2 region filter; empty = all regions.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Gap domains ranked by opportunity."
          }
        },
        "description": "Gap analysis \u2014 sources citing competitors but not you (bearer-authed)"
      }
    },
    "/api/public/v1/industry/competitors/{name}/prompts": {
      "get": {
        "tags": [
          "Public API",
          "Industry"
        ],
        "summary": "Per-competitor prompt drilldown",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/RangeQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Prompts where the competitor appeared."
          }
        },
        "description": "Per-competitor prompt drilldown (bearer-authed)"
      }
    },
    "/api/public/v1/industry/competitors/{name}/citations": {
      "get": {
        "tags": [
          "Public API",
          "Industry"
        ],
        "summary": "Per-competitor citation domains",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/RangeQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Citation domains seen with the competitor."
          }
        },
        "description": "Per-competitor citation domains (bearer-authed)"
      }
    }
  },
  "components": {
    "schemas": {
      "VercelLogRecord": {
        "type": "object",
        "description": "One record from Vercel's log schema. Only the fields we read are listed. A record is treated as an HTTP request ONLY when `proxy` is present with a method \u2014 `source: lambda|edge` also covers function stdout, which carries no proxy object.",
        "properties": {
          "id": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "build",
              "edge",
              "lambda",
              "static",
              "external",
              "firewall",
              "redirect"
            ]
          },
          "host": {
            "type": "string",
            "description": "Deployment hostname; prefer proxy.host"
          },
          "timestamp": {
            "type": "integer",
            "format": "int64",
            "description": "Unix MILLISECONDS"
          },
          "proxy": {
            "type": "object",
            "description": "Present only for records describing a served HTTP request.",
            "properties": {
              "timestamp": {
                "type": "integer",
                "format": "int64",
                "description": "Unix MILLISECONDS"
              },
              "method": {
                "type": "string"
              },
              "host": {
                "type": "string",
                "description": "The customer-facing hostname"
              },
              "path": {
                "type": "string",
                "description": "Request path INCLUDING the query string"
              },
              "userAgent": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "An ARRAY, not a string."
              },
              "referer": {
                "type": "string"
              },
              "region": {
                "type": "string",
                "description": "Vercel edge region (e.g. sfo1) \u2014 NOT a country"
              },
              "statusCode": {
                "type": "integer",
                "description": "-1 is a sentinel (crash / background revalidation), not a status"
              },
              "clientIp": {
                "type": "string",
                "description": "Absent when the team hides IPs in Drains"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message.",
            "example": "invalid url"
          }
        }
      },
      "Brand": {
        "type": "object",
        "description": "A tracked brand as returned by GET /brands and GET /brands/{slug}. Field names verified against a live response \u2014 this schema previously described slug/name/primary_region/regions/created_at, none of which the handler emits.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "brand_slug": {
            "type": "string",
            "description": "Stable identifier used as {slug} in every per-brand path.",
            "example": "linear"
          },
          "brand_name": {
            "type": "string",
            "example": "Linear"
          },
          "domain": {
            "type": "string",
            "example": "linear.app"
          },
          "vertical": {
            "type": "string",
            "description": "One of saas, retail, finance, healthcare, other.",
            "example": "saas"
          },
          "scan_frequency_hours": {
            "type": "integer",
            "description": "Rescan cadence for this brand, set by the account's plan.",
            "example": 24
          },
          "next_scan_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "site_id": {
            "type": "string",
            "format": "uuid",
            "description": "Identifier baked into the agent-analytics JS tag."
          },
          "added_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Snapshot": {
        "type": "object",
        "description": "One day of measured visibility for a brand. Field names verified against a live response \u2014 this schema previously described ai_presence_score and per_engine_presence, neither of which exists.",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "citation_rate": {
            "type": "number",
            "description": "Visibility Score as a fraction between 0 and 1. Multiply by 100 for a percentage \u2014 0.55 is 55%. NOTE the inconsistency with /api/public/v1/model, whose `score` is already on a 0-100 scale.",
            "example": 0.55
          },
          "brand_mentions": {
            "type": "integer",
            "description": "Answers naming the brand. The numerator of citation_rate."
          },
          "total_responses": {
            "type": "integer",
            "description": "Answers captured that day. The denominator. Errored and empty responses are excluded, so an engine outage doesn't read as a visibility drop."
          },
          "per_engine": {
            "type": "object",
            "description": "Per-engine mention rate, keyed by engine slug. Same 0-to-1 scale as citation_rate.",
            "additionalProperties": {
              "type": "number"
            },
            "example": {
              "chatgpt": 0.57,
              "gemini": 0.54
            }
          },
          "sentiment_avg": {
            "type": "number",
            "nullable": true,
            "description": "(positive \u2212 negative) / classified, range \u22121 to +1. Null when nothing was classified."
          },
          "sentiment_pos": {
            "type": "integer"
          },
          "sentiment_neu": {
            "type": "integer"
          },
          "sentiment_neg": {
            "type": "integer"
          },
          "ranking": {
            "type": "object",
            "description": "Mention counts per brand name across the day's answers.",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "ranking_per_engine": {
            "type": "object",
            "description": "The same ranking, split by engine slug.",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "integer"
              }
            }
          },
          "sentiment_per_competitor": {
            "type": "object",
            "description": "Per-competitor sentiment counts, keyed by brand name.",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "pos": {
                  "type": "integer"
                },
                "neu": {
                  "type": "integer"
                },
                "neg": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "Mention": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "engine": {
            "type": "string"
          },
          "prompt": {
            "type": "string"
          },
          "response_snippet": {
            "type": "string"
          },
          "rank": {
            "type": "integer"
          },
          "sentiment": {
            "type": "string",
            "enum": [
              "positive",
              "neutral",
              "negative"
            ]
          },
          "cited_url": {
            "type": "string",
            "format": "uri"
          },
          "seen_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Prompt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "prompt": {
            "type": "string"
          },
          "topic": {
            "type": "string"
          },
          "intent": {
            "type": "string",
            "enum": [
              "informational",
              "commercial",
              "transactional"
            ],
            "description": "Auto-assigned purchase-intent class (deterministic keyword classifier). Always present on prompt-list reads \u2014 persisted at insert, or classified on the fly for rows predating the column.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "source": {
            "type": "string",
            "enum": [
              "",
              "manual",
              "gsc"
            ],
            "description": "Provenance. \"gsc\" = accepted from a Search Console prompt suggestion; \"manual\"/\"\" = user-typed or onboarding.\n"
          },
          "source_query": {
            "type": "string",
            "description": "Originating organic Google query when source=\"gsc\"."
          }
        }
      },
      "CitationDomain": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string"
          },
          "citations": {
            "type": "integer"
          },
          "share": {
            "type": "number",
            "format": "float"
          },
          "bucket": {
            "type": "string",
            "enum": [
              "owned",
              "competitor",
              "third_party"
            ]
          }
        }
      },
      "Citations": {
        "type": "object",
        "properties": {
          "domains": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CitationDomain"
            }
          },
          "total_citations": {
            "type": "integer"
          },
          "range": {
            "type": "string"
          }
        }
      },
      "FanoutEntry": {
        "type": "object",
        "description": "One sub-question + its attributed citation URLs.",
        "properties": {
          "index": {
            "type": "integer",
            "description": "1-based position in the LLM's emitted order \u2014 lower = more prominent."
          },
          "sub_question": {
            "type": "string"
          },
          "citations": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "URLs the engine attributed to this sub-question. Empty array = the engine couldn't tie any citations to this sub-question (a content gap for your brand to fill)."
          }
        }
      },
      "FanoutPromptGroup": {
        "type": "object",
        "description": "All fanouts the LLM extracted for one (prompt \u00d7 engine) snapshot. Returned by /me/citations/fanouts.",
        "properties": {
          "prompt_id": {
            "type": "integer",
            "format": "int64"
          },
          "prompt_text": {
            "type": "string"
          },
          "brand_slug": {
            "type": "string"
          },
          "brand_name": {
            "type": "string"
          },
          "engine": {
            "type": "string"
          },
          "snapshot_date": {
            "type": "string",
            "format": "date"
          },
          "fanouts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FanoutEntry"
            }
          }
        }
      },
      "Portfolio": {
        "type": "object",
        "properties": {
          "brands": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "ai_presence_score": {
                  "type": "integer"
                },
                "delta_7d": {
                  "type": "number"
                },
                "top_engine": {
                  "type": "string"
                },
                "top_competitor": {
                  "type": "string"
                }
              }
            }
          },
          "range": {
            "type": "string"
          },
          "region": {
            "type": "string"
          }
        }
      }
    },
    "parameters": {
      "BrandSlug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9-]+$"
        },
        "description": "URL-safe brand identifier (lowercased, hyphenated).",
        "example": "openai"
      },
      "RangeQuery": {
        "name": "range",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "7d",
            "30d",
            "90d"
          ],
          "default": "30d"
        },
        "description": "Time window for time-series + aggregations."
      },
      "AgentBotQuery": {
        "name": "bot",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Exact bot name (e.g. GPTBot). Part of the page-wide crawler filter: every aggregate on the Agents dashboard honours it, so the KPIs, the trend, the breakdowns and the pages list always describe the same traffic. Combine with `vendor`/`kind` to narrow further (they AND).\n"
      },
      "AgentVendorQuery": {
        "name": "vendor",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string"
        },
        "description": "Exact vendor / platform (e.g. OpenAI) \u2014 every bot that company operates.\n",
        "example": "OpenAI"
      },
      "AgentKindQuery": {
        "name": "kind",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "training_crawler",
            "search_crawler",
            "browsing_agent",
            "agentic_browser",
            "mcp_client"
          ]
        },
        "description": "Exact agent type, e.g. `training_crawler` for bots that collect data for model training vs `browsing_agent` for live answer-time fetches.\n"
      },
      "EngineQuery": {
        "name": "engine",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "chatgpt",
            "claude",
            "gemini",
            "perplexity",
            "copilot",
            "deepseek",
            "grok",
            "ai_overviews",
            "ai_mode"
          ]
        },
        "description": "Filter to a single LLM engine. Matched exactly against the stored\nengine slug, so a vendor name (`openai`, `anthropic`) or a display\nname (`AI Overviews`) matches nothing and returns an empty result\nrather than an error.\n",
        "example": "chatgpt"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid session cookie / bearer token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "fxa_",
        "description": "API keys generated via `POST /api/v1/me/api-keys`. Format:\n`Authorization: Bearer fxa_<32-char-secret>`. Only accepted on\n`/api/public/v1/*`. Cookie auth is rejected on that surface.\n"
      }
    }
  }
}
