{
  "openapi": "3.1.0",
  "info": {
    "title": "AIMentionTracker API",
    "version": "1.0.0",
    "description": "The AI visibility platform to get recommended by AI.\n\nRead how often AI assistants name and cite your brand, and which sites they read instead of yours.\n\nTHREE RULES A CLIENT MUST CARRY, because ignoring them produces confidently wrong reports:\n\n1. `rate: null` means not enough answers yet (below min_sample 5), NOT zero.\n2. Engines are reported separately and never averaged. They disagree, so an average describes no real surface.\n3. An empty /alerts list does NOT mean nothing changed. Findings are suppressed when a move sits inside its own margin of error, which on a small plan is most real moves. GET /me returns the live thresholds.\n\nFailed collections are excluded from denominators rather than counted as absence, so our outages never read as your absence."
  },
  "servers": [
    {
      "url": "https://app.aimentiontracker.ai/api/v1"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "A key from app.aimentiontracker.ai/api-keys, shown once at creation. `Authorization: Bearer <key>` is accepted equivalently. Keys belong to the organisation, not to a person, so they survive that person leaving."
      }
    },
    "schemas": {
      "Rate": {
        "type": "object",
        "properties": {
          "present": {
            "type": "integer",
            "description": "Answers that named this brand."
          },
          "responses": {
            "type": "integer",
            "description": "Answers in the window. The denominator, always returned."
          },
          "rate": {
            "type": [
              "number",
              "null"
            ],
            "description": "present/responses to 3dp, or NULL below min_sample (5). Null means NOT ENOUGH ANSWERS YET and must never be rendered as 0% \u2014 doing so reports a collapse in visibility that did not happen."
          },
          "min_sample": {
            "type": "integer",
            "description": "The floor below which rate is null."
          }
        },
        "required": [
          "present",
          "responses",
          "rate",
          "min_sample"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code. Branch on this, not the message."
              },
              "message": {
                "type": "string",
                "description": "Human-readable. Wording may change; the code will not."
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "BrandRef": {
        "type": "object",
        "description": "Which brand a response describes. Present in meta on every data endpoint.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass as ?brand=."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "Engine": {
        "type": "object",
        "description": "One AI engine's numbers. NEVER average these together.",
        "properties": {
          "engine": {
            "type": "string",
            "description": "Stable slug, e.g. chatgpt. Use this for ?engine= filters."
          },
          "name": {
            "type": "string",
            "description": "Display name, e.g. ChatGPT."
          },
          "collection_method": {
            "type": "string",
            "description": "How this surface is read: api, scraper or serp."
          },
          "present": {
            "type": "integer",
            "description": "Answers that named this brand."
          },
          "responses": {
            "type": "integer",
            "description": "Answers in the window. The denominator, always returned."
          },
          "rate": {
            "type": [
              "number",
              "null"
            ],
            "description": "present/responses to 3dp, or NULL below min_sample (5). Null means NOT ENOUGH ANSWERS YET and must never be rendered as 0% \u2014 doing so reports a collapse in visibility that did not happen."
          },
          "min_sample": {
            "type": "integer",
            "description": "The floor below which rate is null."
          },
          "citations": {
            "type": "integer",
            "description": "Answers from this engine that cited the brand's own domain."
          },
          "avg_rank_when_named": {
            "type": [
              "number",
              "null"
            ],
            "description": "Mean position in the list WHEN NAMED. Null when never named."
          }
        },
        "required": [
          "engine",
          "name",
          "collection_method",
          "present",
          "responses",
          "rate",
          "min_sample"
        ]
      },
      "BrandVisibility": {
        "type": "object",
        "description": "One tracked brand, pooled across engines. The only place we pool.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Brand name."
          },
          "role": {
            "type": "string",
            "enum": [
              "self",
              "competitor"
            ],
            "description": "Yours, or a rival."
          },
          "present": {
            "type": "integer",
            "description": "Answers that named this brand."
          },
          "responses": {
            "type": "integer",
            "description": "Answers in the window. The denominator, always returned."
          },
          "rate": {
            "type": [
              "number",
              "null"
            ],
            "description": "present/responses to 3dp, or NULL below min_sample (5). Null means NOT ENOUGH ANSWERS YET and must never be rendered as 0% \u2014 doing so reports a collapse in visibility that did not happen."
          },
          "min_sample": {
            "type": "integer",
            "description": "The floor below which rate is null."
          },
          "citations": {
            "type": "integer",
            "description": "Answers citing this brand's own domain."
          },
          "avg_rank_when_named": {
            "type": [
              "number",
              "null"
            ],
            "description": "Mean list position when named."
          },
          "framing": {
            "type": "object",
            "description": "How the brand was FRAMED where it appeared. Deliberately not called a sentiment score: it counts cue phrases, it does not grade tone.",
            "properties": {
              "classified": {
                "type": "integer",
                "description": "Mentions we could classify at all. The denominator here."
              },
              "recommended": {
                "type": "integer",
                "description": "Mentions framed as a recommendation."
              },
              "negative": {
                "type": "integer",
                "description": "Mentions framed negatively."
              }
            }
          }
        },
        "required": [
          "name",
          "role",
          "present",
          "responses",
          "rate",
          "min_sample"
        ]
      },
      "Brand": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The workspace public id. Pass as ?brand=."
          },
          "name": {
            "type": "string",
            "description": "Brand name."
          },
          "domain": {
            "type": [
              "string",
              "null"
            ],
            "description": "The brand's own domain, null before setup finishes."
          },
          "configured": {
            "type": "boolean",
            "description": "False means setup was never finished. Data endpoints return 404 brand_not_configured for one of these rather than a page of zeroes, which would read as measured absence."
          },
          "active_prompts": {
            "type": "integer",
            "description": "Prompts currently running for this brand."
          },
          "market": {
            "type": "object",
            "properties": {
              "location_code": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "DataForSEO location code."
              },
              "language_code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ISO language code."
              }
            }
          },
          "last_collected_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO timestamp of the most recent stored answer."
          }
        },
        "required": [
          "id",
          "name",
          "configured",
          "active_prompts"
        ]
      },
      "Mention": {
        "type": "object",
        "properties": {
          "brand": {
            "type": "string",
            "description": "Which tracked brand this refers to."
          },
          "role": {
            "type": "string",
            "enum": [
              "self",
              "competitor"
            ]
          },
          "mentioned": {
            "type": "boolean",
            "description": "False rows exist: we looked and it was absent."
          },
          "rank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Position in the answer's list, 1-based. Null when unranked."
          },
          "count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Times named in this one answer."
          },
          "framing": {
            "type": [
              "string",
              "null"
            ],
            "description": "recommended | neutral | negative | null when genuinely ambivalent."
          },
          "framing_signals": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "The cue phrases behind `framing`, so a judgement can be checked."
          },
          "snippet": {
            "type": [
              "string",
              "null"
            ],
            "description": "The verbatim sentence naming the brand. This is the evidence."
          }
        },
        "required": [
          "brand",
          "role",
          "mentioned"
        ]
      },
      "Citation": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "The cited page."
          },
          "domain": {
            "type": "string",
            "description": "Its host."
          },
          "rank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Position in the engine's source list. That order IS the rank."
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Page title where the engine supplied one."
          }
        },
        "required": [
          "url",
          "domain"
        ]
      },
      "Answer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque public id, ans_<12>. Also the pagination cursor. Never an integer."
          },
          "engine": {
            "type": "string",
            "description": "Engine slug."
          },
          "collection_method": {
            "type": "string",
            "description": "api, scraper or serp."
          },
          "prompt": {
            "type": "string",
            "description": "The prompt that was asked. Called `prompt`, not `question`."
          },
          "answer": {
            "type": [
              "string",
              "null"
            ],
            "description": "The answer verbatim, markdown. Null when the engine returned none."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "ok",
              "empty"
            ],
            "description": "'empty' is a REAL OBSERVATION: the engine answered and named nobody. It is in the denominator everywhere. Failed collections (error, timeout) are excluded entirely and never appear here."
          },
          "collected_at": {
            "type": "string",
            "description": "ISO timestamp."
          },
          "location_code": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The market this was collected in."
          },
          "mentions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Mention"
            }
          },
          "citations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Citation"
            }
          }
        },
        "required": [
          "id",
          "engine",
          "prompt",
          "outcome",
          "collected_at",
          "mentions",
          "citations"
        ]
      },
      "Source": {
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "description": "Normalised host \u2014 www.example.com and example.com are one row."
          },
          "answers_citing_it": {
            "type": "integer",
            "description": "Answers in the window that cited this domain."
          },
          "answers_where_you_were_named": {
            "type": "integer",
            "description": "Of those, how many also named your brand."
          },
          "answers_citing_you_without_naming_you": {
            "type": "integer",
            "description": "The actionable number. A domain read on nine answers that named a competitor every time is a specific page to go and be on."
          }
        },
        "required": [
          "domain",
          "answers_citing_it",
          "answers_where_you_were_named",
          "answers_citing_you_without_naming_you"
        ]
      },
      "CompetitorQuote": {
        "type": "object",
        "properties": {
          "snippet": {
            "type": "string",
            "description": "The sentence the engine wrote, with its ** emphasis left intact."
          },
          "engine": {
            "type": "string",
            "description": "Engine slug the sentence came from."
          },
          "question": {
            "type": "string",
            "description": "The tracked prompt whose answer contained it."
          },
          "listRank": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Where the engine placed them; 1 is first. Null when it wrote no list."
          },
          "answerId": {
            "type": "string",
            "description": "The answer this came from, as returned by /answers."
          }
        },
        "required": [
          "snippet",
          "engine",
          "question",
          "listRank",
          "answerId"
        ]
      },
      "Competitor": {
        "type": "object",
        "properties": {
          "competitor": {
            "type": "string",
            "description": "The competitor's name as tracked in this workspace."
          },
          "domain": {
            "type": [
              "string",
              "null"
            ],
            "description": "Their domain, when one is recorded."
          },
          "answers_naming_them": {
            "type": "integer",
            "description": "Answers in the window that named them."
          },
          "answers_without_you": {
            "type": "integer",
            "description": "The actionable column. Answers that named this competitor and never mentioned you. 'Named in 48 answers' is a fact about them; 'named in 47 answers that never mentioned you' is a fact about you."
          },
          "best_list_position": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Best (lowest) position they reached. Null when no answer ranked them."
          },
          "typical_list_position": {
            "type": [
              "number",
              "null"
            ],
            "description": "Median position to 1dp. Null when no answer ranked them."
          },
          "total_mentions": {
            "type": "integer",
            "description": "Times named across those answers, which can exceed answers_naming_them."
          },
          "engines": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Engine slugs that named them. Listed, never pooled into one figure."
          },
          "quotes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompetitorQuote"
            },
            "description": "Up to three supporting sentences, best-ranked first. PRESENT ONLY with ?quotes=1, and absent rather than empty otherwise."
          }
        },
        "required": [
          "competitor",
          "domain",
          "answers_naming_them",
          "answers_without_you",
          "best_list_position",
          "typical_list_position",
          "total_mentions",
          "engines"
        ]
      },
      "BrandSentiment": {
        "type": "object",
        "properties": {
          "brand": {
            "type": "string",
            "description": "The brand this row describes."
          },
          "is_you": {
            "type": "boolean",
            "description": "True for the workspace's own brand."
          },
          "answers_naming_them": {
            "type": "integer",
            "description": "Answers that named this brand. NOT the denominator."
          },
          "classified": {
            "type": "integer",
            "description": "THE DENOMINATOR. Mentions the classifier was willing to call. It abstains on listicle mentions, where an engine names a brand without endorsing it, so this is usually much smaller than answers_naming_them."
          },
          "recommended": {
            "type": "integer",
            "description": "Of the classified mentions, how many read as a recommendation."
          },
          "negative": {
            "type": "integer",
            "description": "Of the classified mentions, how many read as negative."
          },
          "neutral": {
            "type": "integer",
            "description": "classified - recommended - negative, floored at 0."
          },
          "recommended_share": {
            "type": [
              "number",
              "null"
            ],
            "description": "recommended/classified to 3dp, or NULL below min_sample (5) classified mentions. Null means NOT ENOUGH CLASSIFIED MENTIONS and must never be rendered as 0%."
          }
        },
        "required": [
          "brand",
          "is_you",
          "answers_naming_them",
          "classified",
          "recommended",
          "negative",
          "neutral",
          "recommended_share"
        ]
      },
      "ShareOfVoice": {
        "type": "object",
        "properties": {
          "brand": {
            "type": "string",
            "description": "The brand this row describes."
          },
          "is_you": {
            "type": "boolean",
            "description": "True for the workspace's own brand."
          },
          "mentions": {
            "type": "integer",
            "description": "Answers in the window that named this brand."
          },
          "share": {
            "type": [
              "number",
              "null"
            ],
            "description": "This brand's mentions over every tracked brand's, to 4dp. NULL when no brand was named at all, because a share of an empty pool is undefined. That is not the same as a 0% share."
          }
        },
        "required": [
          "brand",
          "is_you",
          "mentions",
          "share"
        ]
      },
      "Alert": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque public id, alr_<12>. Use it to dedupe across polls."
          },
          "kind": {
            "type": "string",
            "description": "Finding type, e.g. visibility_drop."
          },
          "title": {
            "type": "string",
            "description": "One line."
          },
          "detail": {
            "type": "string",
            "description": "The explanation, including the numbers behind it."
          },
          "facts": {
            "type": "object",
            "description": "The figures the finding was computed from."
          },
          "created_at": {
            "type": "string",
            "description": "ISO timestamp."
          },
          "read": {
            "type": "boolean",
            "description": "Whether it has been seen in the dashboard."
          }
        },
        "required": [
          "id",
          "kind",
          "title",
          "created_at",
          "read"
        ]
      },
      "Me": {
        "type": "object",
        "properties": {
          "organization": {
            "type": "object",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Organisation name."
              }
            }
          },
          "plan": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "description": "trial | solo | starter | growth | agency."
              },
              "status": {
                "type": "string",
                "description": "Stripe subscription status."
              },
              "trial_ends_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ISO timestamp, null when not trialling."
              },
              "prompts": {
                "type": "object",
                "properties": {
                  "used": {
                    "type": "integer",
                    "description": "Active prompts across the organisation."
                  },
                  "included": {
                    "type": "integer",
                    "description": "The plan's cap."
                  }
                }
              },
              "brands": {
                "type": "integer",
                "description": "Brands in the organisation."
              }
            }
          },
          "key": {
            "type": "object",
            "properties": {
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Granted to this key."
              },
              "available_scopes": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Every scope that exists, so 'not granted' is distinguishable from 'not real'."
              },
              "pinned_to_brand": {
                "type": "boolean",
                "description": "True when the key is bound to one brand. Such a key refuses any other ?brand=, and GET /brands returns only that one brand."
              }
            }
          },
          "brand": {
            "$ref": "#/components/schemas/BrandRef"
          },
          "rate_limit": {
            "type": "object",
            "properties": {
              "per_minute": {
                "type": "integer",
                "description": "Requests a minute. 300 on every plan."
              },
              "remaining": {
                "type": "integer",
                "description": "Left in the current minute."
              },
              "resets_in_seconds": {
                "type": "integer",
                "description": "Until the window rolls."
              }
            }
          },
          "reporting_rules": {
            "type": "object",
            "description": "NOT ADVISORY. A client that ignores these produces wrong reports.",
            "properties": {
              "min_sample": {
                "type": "integer",
                "description": "Below this many answers, rate is null."
              },
              "rate_null_means": {
                "type": "string",
                "description": "Plain-language restatement, for surfacing to an end user."
              },
              "engines_averaged": {
                "type": "boolean",
                "description": "Always false. Engines disagree; a mean describes no real surface."
              },
              "alerts": {
                "type": "object",
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "kinds": {
                    "type": [
                      "array",
                      "string"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "description": "Array of requested kinds, or the string 'all' when none are set. Kinds: visibility_drop, visibility_rise, competitor_overtook, cited_not_named, negative_framing, engine_blackout."
                  },
                  "min_delta_points": {
                    "type": "integer",
                    "description": "Percentage points of change required to raise a finding."
                  },
                  "min_sample_each_side": {
                    "type": "integer",
                    "description": "Answers required on BOTH sides of the comparison."
                  },
                  "also_suppressed_below_statistical_noise": {
                    "type": "boolean",
                    "description": "Always true. Even a move clearing min_delta_points is suppressed if it sits inside a 95% CI on the difference of two proportions. Whichever bar is HIGHER wins."
                  },
                  "empty_list_means": {
                    "type": "string",
                    "description": "The honest phrasing for an empty /alerts response."
                  }
                }
              }
            }
          }
        },
        "required": [
          "organization",
          "plan",
          "key",
          "brand",
          "rate_limit",
          "reporting_rules"
        ]
      },
      "Recommendation": {
        "type": "object",
        "description": "One piece of work, with the pages and answers that justify it. A row with nothing to point at is filtered out rather than softened into generic advice.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "own_page",
              "listing",
              "community",
              "marketplace"
            ],
            "description": "What kind of WORK this is, not what kind of site it is. You submit to a listing, you participate in a community, you rewrite your own page this afternoon."
          },
          "tier": {
            "type": "integer",
            "description": "Sort order, and an argument rather than a score: 1 your own page (nobody has to agree), 2 a listing you can submit to this week, 3 a community you must participate in, 4 a marketplace that is usually a procurement decision."
          },
          "site": {
            "type": "string",
            "description": "The registrable site. One publisher, one outreach contact."
          },
          "pages": {
            "type": "array",
            "description": "The specific pages, because 'get listed on medium.com' is advice and naming the roundup is a task. Never empty.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "title": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "required": [
                "url",
                "title"
              ]
            }
          },
          "answers": {
            "type": "integer",
            "description": "Answers in the window that cited this site."
          },
          "answers_naming_you": {
            "type": "integer",
            "description": "Of those, how many named you. Zero for every gap, by definition."
          },
          "competitors_named": {
            "type": "integer",
            "description": "Distinct tracked competitors named in those answers."
          },
          "engines": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Which engines read it. Listed, never pooled into one figure."
          },
          "share": {
            "type": "number",
            "description": "Fraction of meta.total_answers this door accounts for. Meaningless without that denominator, which is why it is returned beside it."
          }
        },
        "required": [
          "kind",
          "tier",
          "site",
          "pages",
          "answers",
          "answers_naming_you",
          "competitors_named",
          "engines",
          "share"
        ]
      },
      "Question": {
        "type": "object",
        "description": "One tracked question. Every other measurement in this API is derived from these, so a disappointing figure may mean the wrong question rather than poor visibility.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Public id."
          },
          "text": {
            "type": "string"
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "description": "How the question got here, for example user or suggested."
          },
          "active": {
            "type": "boolean",
            "description": "Inactive questions stop being run but keep the answers they already produced."
          },
          "answers": {
            "type": "integer",
            "description": "Answers this question has produced in total, all engines."
          },
          "answers_naming_you": {
            "type": "integer"
          },
          "web_search_volume": {
            "type": [
              "integer",
              "null"
            ],
            "description": "NULL MEANS NOT CHECKED, not zero. Reporting an unchecked volume as 0 asserts nobody searches a question nobody has measured. Read volume_checked_at before quoting demand."
          },
          "volume_checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When volume was last looked up. Null when it never has been."
          }
        },
        "required": [
          "id",
          "text",
          "category",
          "source",
          "active",
          "answers",
          "answers_naming_you",
          "web_search_volume",
          "volume_checked_at"
        ]
      }
    }
  },
  "paths": {
    "/me": {
      "get": {
        "summary": "Who this key is, and the rules for reading the numbers",
        "description": "Call this first. The reporting_rules block is not advisory: it carries the sample floor and the live alert thresholds, which is the only way a client can tell 'no alerts' from 'nothing changed'.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Me"
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": {
                    "organization": {
                      "name": "Acme Inc"
                    },
                    "plan": {
                      "name": "growth",
                      "status": "active",
                      "trial_ends_at": null,
                      "prompts": {
                        "used": 7,
                        "included": 9
                      },
                      "brands": 2
                    },
                    "key": {
                      "scopes": [
                        "read"
                      ],
                      "available_scopes": [
                        "read",
                        "prompts:write",
                        "brands:write"
                      ],
                      "pinned_to_brand": false
                    },
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "rate_limit": {
                      "per_minute": 300,
                      "remaining": 299,
                      "resets_in_seconds": 41
                    },
                    "reporting_rules": {
                      "min_sample": 5,
                      "rate_null_means": "not enough answers yet \u2014 NOT zero",
                      "engines_averaged": false,
                      "alerts": {
                        "enabled": true,
                        "kinds": "all",
                        "min_delta_points": 15,
                        "min_sample_each_side": 10,
                        "also_suppressed_below_statistical_noise": true,
                        "empty_list_means": "no change large enough to distinguish from noise at this sample size"
                      }
                    }
                  },
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/brands": {
      "get": {
        "summary": "Every brand this key may read",
        "description": "The ids to pass as ?brand=. A pinned key sees only its own.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Brand"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "pinned": {
                          "type": "boolean",
                          "description": "True when this key is bound to one brand."
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [
                    {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme",
                      "domain": "acme.com",
                      "configured": true,
                      "active_prompts": 5,
                      "market": {
                        "location_code": 2840,
                        "language_code": "en"
                      },
                      "last_collected_at": "2026-09-30T04:12:00.000Z"
                    },
                    {
                      "id": "wsp_094oxa5e",
                      "name": "Acme Labs",
                      "domain": null,
                      "configured": false,
                      "active_prompts": 0,
                      "market": {
                        "location_code": 2840,
                        "language_code": "en"
                      },
                      "last_collected_at": null
                    }
                  ],
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z",
                    "pinned": false,
                    "note": "Pass one of these ids as ?brand= on every other endpoint."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/visibility": {
      "get": {
        "summary": "Presence per engine and per brand over a trailing window",
        "description": "There is no single visibility score and there will not be one. The engines disagree, so an average describes no real surface; compute one yourself if you want it, knowing exactly what went in.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 7
            }
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "engines": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Engine"
                          }
                        },
                        "brands": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BrandVisibility"
                          }
                        }
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "window_days": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": {
                    "engines": [
                      {
                        "engine": "chatgpt",
                        "name": "ChatGPT",
                        "collection_method": "scraper",
                        "present": 18,
                        "responses": 30,
                        "rate": 0.6,
                        "min_sample": 5,
                        "citations": 7,
                        "avg_rank_when_named": 2.4
                      },
                      {
                        "engine": "perplexity",
                        "name": "Perplexity",
                        "collection_method": "api",
                        "present": 3,
                        "responses": 4,
                        "rate": null,
                        "min_sample": 5,
                        "citations": 1,
                        "avg_rank_when_named": 3
                      }
                    ],
                    "brands": [
                      {
                        "name": "Acme",
                        "role": "self",
                        "present": 21,
                        "responses": 34,
                        "rate": 0.618,
                        "min_sample": 5,
                        "citations": 8,
                        "avg_rank_when_named": 2.5,
                        "framing": {
                          "classified": 21,
                          "recommended": 14,
                          "negative": 1
                        }
                      }
                    ]
                  },
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "window_days": 30,
                    "note": "Engines are reported separately and never averaged. The perplexity row above has rate: null because 4 answers is below min_sample \u2014 that means NOT ENOUGH DATA, not 0%."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/answers": {
      "get": {
        "summary": "The stored answers, with their mentions and citations",
        "description": "The raw material every other number traces back to. Keyset pagination: pass meta.next_cursor as ?cursor=. outcome='empty' is a real observation (the engine answered and named nobody), not a failure.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "An answer id from a previous page."
          },
          {
            "name": "engine",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter to one engine slug."
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Answer"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "next_cursor": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Pass as ?cursor=. Null on the last page."
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [
                    {
                      "id": "ans_SfwqC8iCWwte",
                      "engine": "chatgpt",
                      "collection_method": "scraper",
                      "prompt": "best project management tools for agencies",
                      "answer": "For agencies, the most commonly recommended options are\u2026",
                      "outcome": "ok",
                      "collected_at": "2026-09-30T04:12:00.000Z",
                      "location_code": 2840,
                      "mentions": [
                        {
                          "brand": "Acme",
                          "role": "self",
                          "mentioned": true,
                          "rank": 2,
                          "count": 1,
                          "framing": "recommended",
                          "framing_signals": [
                            "best for"
                          ],
                          "snippet": "Acme is best for agencies juggling many clients."
                        },
                        {
                          "brand": "Rival",
                          "role": "competitor",
                          "mentioned": false,
                          "rank": null,
                          "count": 0,
                          "framing": null,
                          "framing_signals": null,
                          "snippet": null
                        }
                      ],
                      "citations": [
                        {
                          "url": "https://example.com/best-pm-tools",
                          "domain": "example.com",
                          "rank": 1,
                          "title": "Best PM tools in 2026"
                        }
                      ]
                    }
                  ],
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "next_cursor": "ans_vIUZWjBPDEm2",
                    "note": "Pass next_cursor as ?cursor= for the following page."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sources": {
      "get": {
        "summary": "The domains the engines read before answering",
        "description": "answers_citing_you_without_naming_you is the actionable column: a source read on nine answers that named a competitor every time is a specific page to go and be on. Hosts are normalised, so www.example.com and example.com are one row.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Source"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "window_days": {
                          "type": "integer"
                        },
                        "limit": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [
                    {
                      "domain": "g2.com",
                      "answers_citing_it": 12,
                      "answers_where_you_were_named": 3,
                      "answers_citing_you_without_naming_you": 9
                    },
                    {
                      "domain": "acme.com",
                      "answers_citing_it": 5,
                      "answers_where_you_were_named": 5,
                      "answers_citing_you_without_naming_you": 0
                    }
                  ],
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "window_days": 30,
                    "limit": 200,
                    "note": "g2.com was read on 12 answers and named you on 3. Those other 9 are the work."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/competitors": {
      "get": {
        "summary": "Who the engines name instead of you",
        "description": "answers_without_you is the column that matters. 'Named in 48 answers' is a fact about a competitor; 'named in 47 answers that never mentioned you' is a fact about you, and it is the one you can act on.\n\nQuotes are OFF by default and capped at three per competitor. They are the evidence behind the number, and pulling them for twenty competitors on every call turns a report into a transcript.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          },
          {
            "name": "quotes",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "Pass 1 to include up to three supporting sentences per competitor. Omitted entirely when not requested."
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Competitor"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "window_days": {
                          "type": "integer"
                        },
                        "total_answers": {
                          "type": "integer",
                          "description": "Answers in the window, all engines."
                        },
                        "answers_naming_you": {
                          "type": "integer"
                        },
                        "answers_without_you": {
                          "type": "integer",
                          "description": "total_answers - answers_naming_you."
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [
                    {
                      "competitor": "Rival",
                      "domain": "rival.com",
                      "answers_naming_them": 48,
                      "answers_without_you": 47,
                      "best_list_position": 1,
                      "typical_list_position": 2.5,
                      "total_mentions": 61,
                      "engines": [
                        "chatgpt",
                        "perplexity"
                      ]
                    },
                    {
                      "competitor": "Otherco",
                      "domain": null,
                      "answers_naming_them": 6,
                      "answers_without_you": 4,
                      "best_list_position": null,
                      "typical_list_position": null,
                      "total_mentions": 6,
                      "engines": [
                        "gemini"
                      ]
                    }
                  ],
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "window_days": 30,
                    "total_answers": 60,
                    "answers_naming_you": 8,
                    "answers_without_you": 52,
                    "note": "answers_without_you is the actionable column: answers that named this competitor and never mentioned you. Engines are listed, never pooled into one figure. Pass quotes=1 for up to three supporting sentences per competitor."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recommendations": {
      "get": {
        "summary": "What to do next, ranked, with the evidence",
        "description": "CHECK `displayable` BEFORE REPORTING ANYTHING. When it is false the list is empty because the corpus is too thin to rank honestly, NOT because there is nothing to do, and reporting that as a clean bill of health is the worst mistake available on this endpoint. Those two states call for opposite reactions.\n\nOrdered by `tier`, which is an editorial judgement about whose permission the work needs rather than a measurement: your own page outranks a marketplace because nobody has to agree to it. There is deliberately NO blended score. A caller who disagrees with the order can see exactly which judgement to argue with, which is not true of 0.734.\n\nEvery row cites the pages behind it. There is no 'write more content' and no 'add schema markup': an item with nothing to point at is filtered out rather than softened into advice.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 12
            }
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Recommendation"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "window_days": {
                          "type": "integer"
                        },
                        "total_answers": {
                          "type": "integer",
                          "description": "The denominator for every share above."
                        },
                        "displayable": {
                          "type": "boolean",
                          "description": "False when the corpus is too thin to rank anything honestly. An empty list with displayable false means NOT YET, never NOTHING TO DO."
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [
                    {
                      "kind": "own_page",
                      "tier": 1,
                      "site": "yoursite.com",
                      "pages": [
                        {
                          "url": "https://yoursite.com/pricing",
                          "title": "Pricing"
                        }
                      ],
                      "answers": 17,
                      "answers_naming_you": 1,
                      "competitors_named": 4,
                      "engines": [
                        "chatgpt",
                        "perplexity"
                      ],
                      "share": 0.28
                    },
                    {
                      "kind": "listing",
                      "tier": 2,
                      "site": "someroundup.com",
                      "pages": [
                        {
                          "url": "https://someroundup.com/best-tools",
                          "title": "The 12 Best Tools"
                        }
                      ],
                      "answers": 9,
                      "answers_naming_you": 0,
                      "competitors_named": 6,
                      "engines": [
                        "gemini"
                      ],
                      "share": 0.15
                    }
                  ],
                  "meta": {
                    "generated_at": "2026-10-13T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "window_days": 30,
                    "total_answers": 60,
                    "displayable": true,
                    "note": "Ordered by tier, which is an editorial judgement about whose permission the work needs, not by volume: your own page outranks a marketplace because nobody has to agree to it. Engines are listed, never pooled. `share` is a fraction of total_answers, which is the only denominator it is comparable against. Every row cites the pages behind it; a recommendation with nothing to point at is filtered out rather than softened into advice."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/questions": {
      "get": {
        "summary": "The questions this workspace runs against the engines",
        "description": "Call this before interpreting a disappointing visibility number. Everything else in this API is derived from these questions, so a low figure may mean the wrong question rather than poor visibility, and the two have opposite fixes.\n\n`web_search_volume` IS NULL WHEN UNCHECKED, not when it is zero. Read `volume_checked_at` before quoting demand: rendering an unchecked volume as 0 asserts nobody searches a question nobody has measured.\n\nThe allowance in meta counts ACTIVE questions across the whole organisation, not per workspace, because a question costs the same vendor spend whichever brand it sits under.",
        "parameters": [
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Question"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "questions_allowed": {
                          "type": "integer",
                          "description": "The plan's cap on active questions, organisation-wide."
                        },
                        "questions_used": {
                          "type": "integer"
                        },
                        "questions_remaining": {
                          "type": "integer",
                          "description": "Zero means another question cannot be added on this plan."
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [
                    {
                      "id": "qst_7Hk2",
                      "text": "best ai visibility tools",
                      "category": "category",
                      "source": "user",
                      "active": true,
                      "answers": 121,
                      "answers_naming_you": 1,
                      "web_search_volume": 390,
                      "volume_checked_at": "2026-10-01T00:00:00.000Z"
                    },
                    {
                      "id": "qst_9Lp4",
                      "text": "how to track brand mentions in chatgpt",
                      "category": null,
                      "source": "suggested",
                      "active": true,
                      "answers": 60,
                      "answers_naming_you": 0,
                      "web_search_volume": null,
                      "volume_checked_at": null
                    }
                  ],
                  "meta": {
                    "generated_at": "2026-10-13T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "questions_allowed": 5,
                    "questions_used": 2,
                    "questions_remaining": 3,
                    "note": "These are the questions every other measurement in this API is derived from, so a disappointing visibility figure may mean the wrong question rather than poor visibility. web_search_volume is null when it has not been checked, NOT when it is zero: check volume_checked_at before reporting demand. The allowance counts active questions across the whole organisation, because a question costs the same vendor spend whichever brand it sits under."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sentiment": {
      "get": {
        "summary": "How the engines frame each brand when they name it",
        "description": "NOT a score. THE DENOMINATOR IS `classified`, NOT `answers_naming_them`, and that is the whole shape of this endpoint. The classifier abstains on most listicle mentions by design: being item four in '12 best tools' is a mention with no opinion attached, and inventing one would be the product making something up. So `classified` is usually much smaller than `answers_naming_them`, and a share computed against the latter understates every brand's positivity by the abstention rate.\n\nThere is no single sentiment number per brand, for the same reason there is no single visibility score: recommended and negative are separate counts over a shared denominator, and collapsing them discards the actionable part.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/BrandSentiment"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "window_days": {
                          "type": "integer"
                        },
                        "min_sample": {
                          "type": "integer",
                          "description": "Classified-mention floor below which recommended_share is null."
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [
                    {
                      "brand": "Acme",
                      "is_you": true,
                      "answers_naming_them": 21,
                      "classified": 9,
                      "recommended": 6,
                      "negative": 1,
                      "neutral": 2,
                      "recommended_share": 0.667
                    },
                    {
                      "brand": "Rival",
                      "is_you": false,
                      "answers_naming_them": 48,
                      "classified": 3,
                      "recommended": 2,
                      "negative": 0,
                      "neutral": 1,
                      "recommended_share": null
                    }
                  ],
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "window_days": 30,
                    "min_sample": 5,
                    "note": "classified is the denominator, not answers_naming_them: the classifier abstains on listicle mentions where the engine named a brand without endorsing it. recommended_share is null below min_sample, which means NOT ENOUGH CLASSIFIED MENTIONS, never zero."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/share-of-voice": {
      "get": {
        "summary": "Each tracked brand's mentions as a share of every tracked brand's",
        "description": "THE ONE PLACE THIS API COMBINES A COMPETITOR'S NUMBER WITH YOURS. Everywhere else engines are reported separately and nothing is averaged; share of voice is a ratio by definition, so the honest move is to publish the denominator beside it rather than refuse the metric.\n\nTHE DENOMINATOR IS THE BRANDS YOU TRACK, NOT THE MARKET. A workspace tracking two competitors and one tracking twelve produce incomparable numbers from identical answers, so always report brands_counted alongside.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ShareOfVoice"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        },
                        "window_days": {
                          "type": "integer"
                        },
                        "total_mentions": {
                          "type": "integer",
                          "description": "The denominator. Sum of every tracked brand's mentions."
                        },
                        "brands_counted": {
                          "type": "integer",
                          "description": "How many brands went into the denominator."
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [
                    {
                      "brand": "Rival",
                      "is_you": false,
                      "mentions": 48,
                      "share": 0.6957
                    },
                    {
                      "brand": "Acme",
                      "is_you": true,
                      "mentions": 21,
                      "share": 0.3043
                    }
                  ],
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "window_days": 30,
                    "total_mentions": 69,
                    "brands_counted": 2,
                    "note": "The denominator is the brands THIS workspace tracks, not the market. Two workspaces tracking different competitor sets produce incomparable shares from the same answers, so always report brands_counted alongside. share is null when no brand was named at all, which is not the same as a 0% share."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/alerts": {
      "get": {
        "summary": "Findings about your brand, newest first",
        "description": "AN EMPTY LIST DOES NOT MEAN NOTHING CHANGED. A finding is raised only when both comparison windows carry enough answers AND the change is larger than its own margin of error. Read an empty list as 'no change large enough to distinguish from noise at this sample size'. GET /me returns the thresholds.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Which brand to report on, from GET /brands. REQUIRED when the organisation has more than one: omitting it returns 400 brand_required rather than defaulting, because a silent default files one customer's numbers under another's. Ignored by a key pinned to a single brand, which refuses any other."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Alert"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "generated_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "brand": {
                          "$ref": "#/components/schemas/BrandRef"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "meta"
                  ]
                },
                "example": {
                  "data": [],
                  "meta": {
                    "generated_at": "2026-09-30T09:00:00.000Z",
                    "brand": {
                      "id": "wsp_zD6cssxGlJyF",
                      "name": "Acme"
                    },
                    "note": "An empty list does NOT mean nothing changed. It means no move was large enough to distinguish from noise at this sample size."
                  }
                }
              }
            }
          },
          "400": {
            "description": "brand_required, or a cursor that does not belong to this brand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The organisation has never subscribed, or the plan excludes the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the required scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand, or the brand has not finished setup.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Carries Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "x-scopes": {
    "read": {
      "capability": "view_reports"
    },
    "prompts:write": {
      "capability": "manage_questions"
    },
    "brands:write": {
      "capability": "manage_brands"
    }
  },
  "x-error-codes": {
    "not_available": {
      "status": 404,
      "meaning": "This deployment does not have the integration the endpoint needs.",
      "fix": "Treat it as permanent for this host rather than retrying: the integration is absent from the deployment, not from your account, so no key and no plan will change the answer. It is returned before your key is checked on purpose, because which integrations a deployment offers is not a per-customer secret, and a 401 first would send you looking for credentials that would not help."
    },
    "unauthorized": {
      "status": 401,
      "meaning": "No key, or a key that is malformed, revoked or expired.",
      "fix": "Create a key at /api-keys. Send it as X-API-Key or Authorization: Bearer."
    },
    "no_subscription": {
      "status": 402,
      "meaning": "This organisation has never had a plan.",
      "fix": "Choose a plan. A lapsed plan is not refused: if you have subscribed before, you keep read access to the answers you already own."
    },
    "api_not_on_plan": {
      "status": 402,
      "meaning": "This plan does not include API access.",
      "fix": "Every plan includes the API, so this should not occur in practice. If you see it, contact support."
    },
    "insufficient_scope": {
      "status": 403,
      "meaning": "The key was not granted the scope this endpoint needs.",
      "fix": "Mint a key with the required scope. Every current endpoint needs `read`."
    },
    "brand_required": {
      "status": 400,
      "meaning": "The organisation has several brands and the request did not say which.",
      "fix": "Pass ?brand=<id>. GET /brands lists them. Deliberately not defaulted: a silent default files one customer's numbers under another's."
    },
    "no_such_brand": {
      "status": 404,
      "meaning": "No brand with that id that this key may read.",
      "fix": "404 rather than 403 on purpose \u2014 a 403 would confirm the brand exists in somebody else's organisation."
    },
    "no_brand": {
      "status": 404,
      "meaning": "This organisation has no brand at all, or the brand a pinned key is bound to has been deleted.",
      "fix": "Finish setup in the dashboard. For a pinned key whose brand is gone, mint a new key."
    },
    "brand_not_configured": {
      "status": 404,
      "meaning": "The brand exists but setup was never finished, so there is nothing to report.",
      "fix": "404 rather than a page of zeroes, which would read as measured absence."
    },
    "bad_cursor": {
      "status": 400,
      "meaning": "The pagination cursor does not belong to this brand.",
      "fix": "Start again without a cursor. Not a silent restart from the top, which would produce undetectable duplicates."
    },
    "rate_limited": {
      "status": 429,
      "meaning": "Either the per-key quota of 300 requests a minute, or the per-address limit of 200 failed authentications an hour, which is applied before the key is checked. Retry-After says how long to wait.",
      "fix": "Back off using Retry-After. If you are seeing this with a key you believe is valid, it is the second cause: the key is being rejected."
    }
  },
  "x-rate-limit": {
    "per_minute": 300,
    "uniform_across_plans": true,
    "headers": [
      "X-RateLimit-Limit",
      "X-RateLimit-Remaining",
      "X-RateLimit-Reset"
    ]
  }
}
