{"openapi":"3.1.0","info":{"title":"RankZero REST API","version":"1.0.0","summary":"Per-brand AI search visibility, competitors, citations, mentions, prompts, Search Console, GA4, Bing Webmaster and technical audit data.","description":"The RankZero REST API gives automations and AI agents server-to-server JSON access to the data RankZero tracks for a brand: AI search visibility and the competitor ranking behind it, the sources AI answers cite, which competitors each AI answer mentioned, the tracked prompts and the prompt suggestion queue, plus the brand's connected Google Search Console, Google Analytics 4 and Bing Webmaster Tools data.\n\nEvery request is scoped to the account that owns the API key: a key reads (and, on the write endpoints, changes) only the brands that account owns or shares via its organization. Start with `GET /brands` to discover the brands and their ids, then call the per-brand endpoints under `/brands/{brand}`.\n\nSuccessful responses return the resource JSON directly. Errors are flat JSON objects of the form `{ \"error\": \"message\" }`; write endpoints add an `errors` array naming each refused input, and plan-limit refusals add the numbers behind the refusal.\n\nIntegration-backed fields (Search Console, GA4, Bing) are `null` or empty when that integration is not connected for the brand, never an error. The same data is also available to AI assistants through the RankZero MCP server.","contact":{"name":"RankZero","email":"support@rankzero.io","url":"https://www.rankzero.io/docs/api-reference"}},"externalDocs":{"description":"RankZero REST API reference","url":"https://www.rankzero.io/docs/api-reference"},"servers":[{"url":"https://www.rankzero.io/api/v1"}],"security":[{"apiKey":[]}],"tags":[{"name":"Brands","description":"Discover the brands the API key can access."},{"name":"Visibility","description":"AI search visibility and the industry ranking of the brand against its tracked competitors."},{"name":"Google Search Console","description":"Search Console totals and dimension breakdowns. Data settles about 2-3 days behind."},{"name":"Google Analytics 4","description":"GA4 sessions by source, daily organic and AI series, events, key events and revenue."},{"name":"Competitors","description":"The competitors tracked for a brand."},{"name":"Citations","description":"Sources cited in AI answers for the brand."},{"name":"Mentions","description":"Every AI answer (prompt run) in a window with the competitors it named."},{"name":"Prompts","description":"Tracked prompts: list, create, activate, retag."},{"name":"Prompt suggestions","description":"The queue of candidate prompts from every engine, distinct from tracked prompts."},{"name":"Bing Webmaster","description":"Bing query performance, keyword research, crawl and index health, URL submission."},{"name":"Technical audit","description":"The weekly crawl's findings: the sitewide record, the to-dos with their level, and each sitemap page's audit facts and index state."},{"name":"Knowledge base","description":"Search the RankZero docs, glossary, guides and blog."}],"paths":{"/brands":{"get":{"operationId":"listBrands","summary":"List accessible brands","description":"Lists the active brands the API key can access (owned by the key's account or shared via its organization), ordered by name. Call this first to discover brand ids; every per-brand endpoint takes the `id` as its `{brand}` path segment. Never lists brands outside the key's scope.","tags":["Brands"],"responses":{"200":{"description":"The accessible active brands.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Brand"}},"example":[{"id":"3f8a1c2e-9b4d-4e7a-bc1f-2d6e5a0b9c11","name":"Tesla","domain":"tesla.com"}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/search":{"get":{"operationId":"searchKnowledgeBase","summary":"Search the RankZero knowledge base","description":"Full-text search over the RankZero docs, glossary, guides and blog. Not brand-scoped. Use it to look up what a metric means or how a feature works before interpreting brand data. A search backend failure returns an empty `results` array rather than an error.","tags":["Knowledge base"],"parameters":[{"name":"query","in":"query","required":false,"description":"The search text. Required in practice: either `query` or its alias `q` must be a non-empty string, otherwise the call returns 400 `query is required`.","schema":{"type":"string","minLength":1},"example":"visibility"},{"name":"q","in":"query","required":false,"description":"Alias for `query`, used only when `query` is absent.","schema":{"type":"string"}},{"name":"locale","in":"query","required":false,"description":"Optional content locale to search in.","schema":{"type":"string"}}],"responses":{"200":{"description":"Matching pages, headings and text passages.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/kpis":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"getBrandKpis","summary":"Headline KPI snapshot","description":"One-call snapshot for a brand: own-brand AI visibility (percent and industry rank), Google Search Console totals, and GA4 sessions by source with RankZero's AI verdict per source. Use it for a quick health read or a report header.\n\n- `gsc` is `null` when Search Console is not connected (or the upstream call failed); `ga4` is `null` when GA4 is not connected. Visibility is still returned.\n- `gscWindow` is the date range the Search Console figures actually cover. Search Console settles about 2-3 days behind, so it ends on the last settled day and is NOT the last N days from today the way visibility and GA4 are. Label GSC numbers with `gscWindow`.\n- `compare=previous` adds `gscPrev` and `comparePeriod` for the same-length preceding window (Search Console only; visibility and GA4 stay single-window).\n- `tag` narrows visibility only. `gsc` and `ga4` are site-wide and unchanged by it.","tags":["Visibility","Google Search Console","Google Analytics 4"],"parameters":[{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/VisibilityTag"},{"$ref":"#/components/parameters/CompareGsc"}],"responses":{"200":{"description":"The KPI snapshot.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KpisResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/overview":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"getBrandOverview","summary":"Visibility and industry ranking","description":"Own-brand AI visibility plus the full industry ranking: every tracked competitor (own brand included) with its rank, visibility percentage and sentiment over the period. Use it to answer \"who wins AI answers in this market and where do we stand\".\n\n- Visibility is a share of the answers that mentioned someone on the tracked list, so adding competitors changes every row, including past periods.\n- `tag` narrows the ranking to runs of active prompts carrying that tag. A tagged percentage is a share within that prompt set, so tag percentages do not sum to the untagged one. The filter reads prompt tags at query time, so retagging a prompt moves its history too.\n- `visibility` is `null` when the ranking has no own-brand row.","tags":["Visibility"],"parameters":[{"$ref":"#/components/parameters/Period"},{"$ref":"#/components/parameters/VisibilityTag"}],"responses":{"200":{"description":"The visibility summary and ranking.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverviewResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/gsc":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"getBrandSearchConsole","summary":"Google Search Console totals or breakdown","description":"Google Search Console performance for the brand's connected property.\n\n- Without `dimensions`: site totals in `gsc` (clicks, impressions, average position, CTR).\n- With `dimensions` (comma-separated): per-row performance in `rows` instead of `gsc`, the raw material for opportunity scans such as near-miss queries at position 11-20 or high-impression, low-CTR pages. Unknown dimension names are ignored; if none are valid the call returns 400.\n- `compare=previous` adds the same-length window immediately before (`comparePeriod`). On totals it adds `gscPrev`; on a breakdown each row gains `clicksPrev`, `impressionsPrev` and `positionPrev`. A `*Prev` of 0 means the row was absent in the previous window (for `positionPrev`: did not rank). Rows are current-window rows only. With `date` among the dimensions the previous window is aligned day for day.\n\n`window` is the range the numbers actually cover. Search Console data lands with a 2-3 day lag, so the window ends on the last settled day, not today: label numbers with `window`, not `period`. `gsc`, `rows` and `window` are `null` when Search Console is not connected or the upstream call failed. Summed breakdown rows undercount site totals because Search Console anonymizes rare queries; use the totals compare for honest period-over-period headlines. Long windows (`6m`, `12m`, `16m`) are reliable for totals; breakdowns are a single request capped by `limit`, not paginated.","tags":["Google Search Console"],"parameters":[{"$ref":"#/components/parameters/Period"},{"name":"dimensions","in":"query","required":false,"description":"Comma-separated breakdown dimensions. When present and non-empty, the response carries `dimensions` and `rows` instead of `gsc`; an empty value returns the totals.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["query","page","date","country","device","searchAppearance"]}},"example":["query"]},{"name":"limit","in":"query","required":false,"description":"Maximum rows for a dimensioned breakdown (clamped to 1-25000). Ignored without `dimensions`.","schema":{"type":"integer","minimum":1,"maximum":25000,"default":1000}},{"$ref":"#/components/parameters/CompareGsc"}],"responses":{"200":{"description":"Totals (no `dimensions`) or a breakdown (`dimensions` given).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GscResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/ga4":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"getBrandGa4","summary":"Google Analytics 4 traffic, events and conversions","description":"GA4 data for the brand's connected property. `window` is the date range covered: GA4 has no reporting lag, so it is `period` applied to today, or the explicit `from`/`to` range.\n\n- Default: `ga4` with sessions by source. Every source is returned; each row carries `aiSessions` (the part that counts as AI search), `ai` and `aiSource` (assistant display name).\n- `dimensions=date`: `rows` with one zero-filled row per day (`sessions`, `organicSessions`, `aiSessions`), ascending. Replaces `ga4`.\n- `dimensions=eventName`: `rows` with every event name received (`count`, `organicCount`, `aiCount`), sorted by count descending. Replaces `ga4`. Use it to check the property is measuring anything at all.\n- `dimensions=landingPage`: `rows` with one row per landing page (`sessions`, `organicSessions`, `aiSessions`), sorted by sessions descending, plus `truncated`. Replaces `ga4`. Shows which pages AI traffic arrived on; pull the window before a change and the window after to see whether a rise landed on the pages that changed. `truncated` is true when a report hit GA4's 10,000-row cap, and a 0 in that case is not a measured zero.\n- `keyEvents=true`: adds `keyEvents` with key events (conversions) per event name, per-audience conversion rates and revenue. Six extra GA4 reports. Rows are empty when no event is marked as a key event in GA4.\n- `directEstimate=true`: adds `directEstimate`, an estimate of AI traffic hidden in direct traffic. Quote only `excess`, which is 0 on most properties. Three extra GA4 reports.\n\nAI Search is GA4's AI Assistant channel plus RankZero's assistant allowlist; Organic Search is GA4's channel plus engines GA4 leaves out. The two are disjoint, and Google AI Overviews and AI Mode count as organic. `ga4`, `rows`, `keyEvents` and `directEstimate` are `null` when GA4 is not connected or the upstream call failed. GA4 limits concurrency per property: keep repeated calls against one brand sequential.","tags":["Google Analytics 4"],"parameters":[{"name":"period","in":"query","required":false,"description":"Rolling reporting window ending today. Unrecognized values fall back to `7d`. Ignored when `from` and `to` are given, in which case `period` comes back `null`.","schema":{"$ref":"#/components/schemas/Period"}},{"name":"from","in":"query","required":false,"description":"Inclusive start of an explicit calendar range (YYYY-MM-DD). Must be given together with `to`; the range may span at most 400 days and applies to every part of the response, `keyEvents` included.","schema":{"type":"string","format":"date"},"example":"2026-05-01"},{"name":"to","in":"query","required":false,"description":"Inclusive end of the explicit calendar range (YYYY-MM-DD). Clamped to today; the resolved range is echoed in `window`.","schema":{"type":"string","format":"date"},"example":"2026-05-31"},{"name":"dimensions","in":"query","required":false,"description":"Exactly one of `date`, `eventName` or `landingPage`. Anything else (including an empty value) is a 400. A dimensioned response carries `dimensions` and `rows` instead of `ga4`.","schema":{"type":"string","enum":["date","eventName","landingPage"]}},{"name":"keyEvents","in":"query","required":false,"description":"Set to `true` (or `1`) to add the `keyEvents` block. Any other value leaves it off.","schema":{"type":"boolean","default":false}},{"name":"directEstimate","in":"query","required":false,"description":"Set to `true` (or `1`) to add the `directEstimate` block. Any other value leaves it off.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"GA4 data for the window.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ga4Response"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/competitors":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"listBrandCompetitors","summary":"List tracked competitors","description":"The brand's tracked competitors, ordered by name, including the brand's own row (`isOwnBrand: true`). `url` is the bare domain and `variations` is the set of strings mention matching keys on. Competitor ids here match `brands_mentioned_ids` in `/mentions`.","tags":["Competitors"],"responses":{"200":{"description":"The tracked competitors.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompetitorsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createBrandCompetitors","summary":"Track competitors","description":"Tracks up to 50 competitors in one call. Per-item, not all-or-nothing: duplicates and invalid items are reported in `errors` while the rest are created. Duplicates are decided on the normalized domain, against existing competitors and earlier items in the same batch; the brand's own domain is refused. A batch that is entirely duplicates is a 200 with `created: 0`; a body with nothing structurally valid is a 400 carrying `errors`.\n\nThe plan's competitor limit is the one thing that fails the whole call: a 409 with `plan`, `limit`, `current` and `requested`. `trackedCompetitors` is the plan budget after the call (the own-brand row never counts).\n\nEach new competitor is backfilled against the brand's 250 most recent answers after the response is sent (best effort). Adding competitors changes visibility for every row, and restates recent past periods too, so add deliberately and between reporting periods. `source` is recorded as `manual`, so API-created competitors are not yet distinguishable from dashboard entries.","tags":["Competitors"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompetitorsRequest"},"example":{"competitors":[{"name":"Rivian","url":"https://www.rivian.com/r1s","variations":["Rivian Automotive"]}]}}}},"responses":{"200":{"description":"The call was processed. `created` may be 0 when every item was a duplicate; see `errors`.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCompetitorsResponse"}}}},"400":{"description":"Malformed body (`body must be { competitors: [{ name, url, variations? }] }`, `competitors must not be empty`, `too many competitors (max 50 per request)`), or `no valid competitors to create` with `errors` naming each refused item.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompetitorItemsError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Either the brand identifier is ambiguous (`ambiguous brand, use id`) or the batch would exceed the plan's competitor limit (plan-limit body with the numbers). Nothing is created.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PlanLimitError"},{"$ref":"#/components/schemas/Error"}]},"example":{"error":"Competitor limit reached: 18 of 20 tracked on the starter plan, 5 more requested.","plan":"starter","limit":20,"current":18,"requested":5}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/citations":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"listBrandCitations","summary":"Sources cited in AI answers","description":"The URLs AI answers cited for the brand's tracked prompts over the period, ranked by how many successful runs cited them, with the share of runs (`usage_pct`), average citations, the prompts each appeared in and a content-type category. Use it to find which pages and publishers shape AI answers in the market (outreach and content targets).","tags":["Citations"],"parameters":[{"$ref":"#/components/parameters/Period"},{"name":"limit","in":"query","required":false,"description":"Maximum number of sources to return.","schema":{"type":"integer","minimum":1,"default":50}},{"name":"country","in":"query","required":false,"description":"Only runs for this country. Matched exactly against the country stored on each run, which is the same full name `/prompts` returns (for example `United States`).","schema":{"type":"string"}},{"name":"provider","in":"query","required":false,"description":"Only runs from this AI provider. A value outside the enum causes a database error (500).","schema":{"$ref":"#/components/schemas/AiProvider"}}],"responses":{"200":{"description":"Cited sources.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CitationsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/mentions":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"listBrandMentions","summary":"AI answers and the competitors they named","description":"Every prompt run in the period, newest first (by `run_at`, then `run_id`), with the tracked competitors (own brand included) its answer mentioned. Paginated. `count` is the total number of runs in the period, not the page size, so \"named in N of M AI answers\" is answerable: M is `count`, N is what you count across the pages.\n\n- Walking `page=1..n` sees every run once. A page past the last returns an empty `mentions` array with the real `count`, not an error.\n- `window.from` is the resolved lower bound; `window.to` equals `asOf`.\n- A run whose answer was never stored is neither listed nor counted.\n- `isOwnBrand` per competitor comes from `/competitors`.","tags":["Mentions"],"parameters":[{"$ref":"#/components/parameters/Period"},{"name":"page","in":"query","required":false,"description":"1-based page number. Values below 1 become 1.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"pageSize","in":"query","required":false,"description":"Rows per page, clamped to 1-500.","schema":{"type":"integer","minimum":1,"maximum":500,"default":50}},{"name":"limit","in":"query","required":false,"deprecated":true,"description":"Older name for `pageSize`, used only when `pageSize` is absent.","schema":{"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"A page of runs with their mentions.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MentionsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/prompts":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"listBrandPrompts","summary":"List tracked prompts","description":"The brand's tracked prompts (the ones being monitored), newest first, 50 per page. Distinct from the suggestion queue at `/prompt-suggestions`. Use `tag` to list the exact prompt set a tagged `/kpis` or `/overview` figure was computed over. Stop paging once `page * pageSize >= count`.","tags":["Prompts"],"parameters":[{"name":"page","in":"query","required":false,"description":"1-based page number (50 prompts per page).","schema":{"type":"integer","minimum":1,"default":1}},{"name":"search","in":"query","required":false,"description":"Case-insensitive substring match on the prompt text.","schema":{"type":"string"}},{"name":"isActive","in":"query","required":false,"description":"Which set to return: active (`true`), inactive (`false`) or both (`all`). Any other value is a 400 `isActive must be 'true', 'false' or 'all'`.","schema":{"type":"string","enum":["true","false","all"],"default":"true"}},{"name":"tag","in":"query","required":false,"description":"Only prompts carrying this tag. Tags are free text, so an unknown tag is an empty page rather than an error.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of tracked prompts.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptsListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createBrandPrompts","summary":"Create tracked prompts","description":"Creates up to 100 tracked prompts in one call. Per-item, not all-or-nothing: duplicates and invalid items are reported in `errors` while the rest are created.\n\n- `country` defaults to the brand's own country; a value you send must resolve to a real country (`US`, `us` and `United States` all work) and is stored as the full name.\n- Duplicates are matched on brand + country + case-insensitive text, including inactive prompts: the existing id comes back as `existingPromptId`, so re-running against an overlapping set is safe. PATCH an inactive one active instead of recreating it.\n- A batch that is entirely duplicates is a 200 with `created: 0`; a body with nothing structurally valid is a 400 carrying `errors`.\n- The plan's active-prompt limit fails the whole call with a 409 carrying `plan`, `limit`, `current` and `requested`. Prompts created with `isActive: false` do not spend budget. `activePrompts` is the budget after the call.\n- `source` is recorded as `manual`, so API-created prompts are not yet distinguishable from dashboard entries.","tags":["Prompts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptsRequest"},"example":{"prompts":[{"text":"best electric SUV","country":"US","tags":["commercial"],"isActive":true}]}}}},"responses":{"200":{"description":"The call was processed. `created` may be 0 when every item was a duplicate; see `errors`.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePromptsResponse"}}}},"400":{"description":"Malformed body (`body must be { prompts: [{ text, country?, tags?, isActive? }] }`, `prompts must not be empty`, `too many prompts (max 100 per request)`), or `no valid prompts to create` with `errors` naming each refused item.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptItemsError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/AmbiguousOrPlanLimit"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateBrandPrompts","summary":"Activate, deactivate or edit tracked prompts","description":"Two body shapes; send one or the other, never both.\n\n- Bulk status `{ promptIds, isActive }`: retire or restore prompts.\n- Per-prompt edits `{ updates: [...] }`: `tags` replaces the tag set, `addTags`/`removeTags` amend it (prefer amending so a concurrent dashboard edit is not clobbered; sending both for one prompt is refused), `country` follows the same recognition rule as create, `isActive` toggles status.\n\nAt most 100 ids or updates per call. Ids are resolved within the addressed brand only: an id from another brand is reported as `unknown prompt for this brand` and not written. Naming the same id twice refuses the second. Activating prompts spends plan budget and can return the same 409 as create; deactivating never does. The response mirrors create: `updated`, the prompts as written, `activePrompts` and per-item `errors`. A call where nothing matched is a 400 that still lists why each id was refused.","tags":["Prompts"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/UpdatePromptsStatusRequest"},{"$ref":"#/components/schemas/UpdatePromptsEditsRequest"}]},"examples":{"bulkStatus":{"summary":"Retire two prompts","value":{"promptIds":["0b6f7a3e-2f7c-4d1e-9b1a-5c2d3e4f5a6b","7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f"],"isActive":false}},"edits":{"summary":"Per-prompt edits","value":{"updates":[{"id":"0b6f7a3e-2f7c-4d1e-9b1a-5c2d3e4f5a6b","addTags":["permits-commercial"],"removeTags":["unclustered"]},{"id":"7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f","tags":["only-these"],"country":"DE"}]}}}}}},"responses":{"200":{"description":"The updates that were applied, plus per-item errors.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePromptsResponse"}}}},"400":{"description":"Malformed body (for example `send either { promptIds, isActive } or { updates }, not both`, `isActive must be a boolean`, `promptIds must not be empty`, `updates must not be empty`, `too many updates (max 100 per request)`), or `no valid updates` / `no prompts matched for this brand` with `errors` naming each refused item.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptItemsError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/AmbiguousOrPlanLimit"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/prompt-suggestions":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"listBrandPromptSuggestions","summary":"List the prompt suggestion queue","description":"Candidate prompts the brand could add to tracking, newest first, across every engine. Distinct from tracked prompts at `/prompts`. `suggestionType` says where each came from: `direct_conversion_gsc` / `direct_conversion_bing` (queries the site already earns, per engine; bare `direct_conversion` is the legacy GSC value), `related_keywords_bing` (Bing keyword research), `keyphrase_expansion_paa` / `keyphrase_expansion_gpt` / `keyphrase_expansion_fallback` (variations hanging off an anchor via `anchorQuery`), `perplexity_related_question` and `gemini_grounding_query`. `source` carries the search demand behind the suggestion where the engine reports it. Bing keyword suggestions live here: filter with `type=direct_conversion_bing,related_keywords_bing`. Not paginated.","tags":["Prompt suggestions"],"parameters":[{"name":"type","in":"query","required":false,"description":"Filter on `suggestionType`: one value or a comma-separated list. `all` (or omitting it) applies no filter. Unknown values return an empty list.","schema":{"type":"string"},"example":"direct_conversion_bing,related_keywords_bing"},{"name":"status","in":"query","required":false,"description":"Filter on `status` (stored values are `suggested`, `accepted` and `rejected`): one value or a comma-separated list. `all` (or omitting it) applies no filter.","schema":{"type":"string"},"example":"suggested"}],"responses":{"200":{"description":"The matching suggestions.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptSuggestionsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/audit":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"getBrandAudit","summary":"Technical audit: sitewide record, to-dos and pages","description":"The brand's technical audit from the weekly crawl, in one read.\n\n- `site`: the newest sitewide record, one per crawl: robots.txt and the AI crawler policy it sets, the sitemap, llms.txt, the favicon, the homepage's Organization and WebSite JSON-LD, the share of crawled pages with JSON-LD, HTTPS, how the www and non-www hosts resolve, `performance` (the origin's Chrome UX Report record and the homepage's lab run) and `authority` (Domain Rating by Ahrefs with its attribution). `null` before the first crawl drains.\n- `todos`: the same list the dashboard's to-dos page shows, from the same server-side read: the latest audit of every URL the sitemap lists, however many crawls ago it ran, so a page the rotating crawl did not reach this week keeps its findings. Each carries `level`: `site` (addressed to `Sitewide`, shown once) or `page`, and a crawler page finding carries `auditedAt` (and per page in `audits`), the date of the audit it came from: a finding dated before the latest crawl is not re-audited yet, which is not the same as fixed. A template finding on a group of more than 50 similar URLs (for example `/products/*`) is reported once for the group, with every affected page in `urls` and the sample in `group`. `todosTruncated` is true when a site's audited pages exceeded the 20,000 the read covers.\n- `pages`: every URL the sitemap lists, sorted, `limit` per call. Each carries the newest audit's facts (`audited: false` and nulls for a page not audited yet), why the crawl last selected it, its template group, its index state per engine, its `crux` reading and `lab` run, and its `images` over 200 KB, each rated `needs-improvement` (to 500 KB) or `poor`.\n- `sitemapTotal`, `total` (URLs the last Search Console inspection run checked) and `checkedAt` (when it ran): a page outside that run is `not_inspected`, never indexed.\n\nIndex state per engine is one of `indexed`, `not_indexed` (with the coverage `reason`), `crowded_out` (inspected, but the cached issues do not name every page the run found not indexed, so this one cannot be called indexed) or `not_inspected`. An engine that is not connected is `null`. Bing's cached status has no per-URL verdict, so its pages are `not_inspected`; use `GET /brands/{brand}/bing` for live lookups.\n\nPage through `pages` with `nextCursor`. `todos` and `site` are the same on every page. `url` returns the one sitemap page matching it (exactly, or ignoring www and a trailing slash), or an empty `pages` when the sitemap does not list it.","tags":["Technical audit"],"parameters":[{"name":"limit","in":"query","required":false,"description":"Sitemap pages per call, clamped to 1-500. Unparseable values fall back to 100.","schema":{"type":"integer","minimum":1,"maximum":500,"default":100}},{"name":"cursor","in":"query","required":false,"description":"The `nextCursor` of the previous response. An unreadable cursor returns 400 `invalid cursor`.","schema":{"type":"string"}},{"name":"url","in":"query","required":false,"description":"Return only this sitemap page. Takes precedence over `cursor`.","schema":{"type":"string","format":"uri"},"example":"https://tesla.com/model-y"}],"responses":{"200":{"description":"The audit.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/bing":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"get":{"operationId":"getBrandBing","summary":"Bing Webmaster performance, research, crawl and index health","description":"Everything from the brand's Bing Webmaster Tools connection, fetched live. Bing is the index Copilot and ChatGPT search lean on.\n\n- `queries`: Bing query performance rolled up to one row per query, same shape as the `/gsc` query breakdown, sorted by impressions.\n- `related`: keyword research, only when `seed` is given: related keywords with trailing 30-day impression volume, sorted by volume.\n- `crawlStats`: the freshest daily crawl breakdown (`inIndex` is the aggregate index count).\n- `crawlIssues`: one row per (url, issue).\n- `indexStatus`: live per-URL index coverage for a page of the brand's sitemap URLs. `indexed` is tri-state: `true` (Bing reports a crawl date), `false` (confident not-indexed verdict) or `null` (lookup throttled or failed: unknown, never treat as not indexed). Use it to find non-indexed URLs, then submit them with `POST /brands/{brand}/bing/submit`.\n\nA brand without a usable Bing connection is still a 200 with `connected: false`, empty or null live fields and `authError` saying why (not linked, no site selected, or a credential Bing no longer accepts). Each live part degrades to empty or null on its own error. A full 100-URL index page can take around 15 seconds.","tags":["Bing Webmaster"],"parameters":[{"name":"seed","in":"query","required":false,"description":"Keyword to expand into related keywords (`related`). Without it `related` is empty and `seed` echoes `null`.","schema":{"type":"string"},"example":"electric suv"},{"name":"country","in":"query","required":false,"description":"Lowercase ISO 3166-1 alpha-2 country for keyword research. Only used with `seed`.","schema":{"type":"string","default":"us"}},{"name":"language","in":"query","required":false,"description":"Full locale for keyword research (a bare language such as `en` is rejected by Bing and yields no rows). Only used with `seed`.","schema":{"type":"string","default":"en-US"}},{"name":"page","in":"query","required":false,"description":"1-based page of sitemap URLs for `indexStatus`.","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","required":false,"description":"Sitemap URLs inspected per `indexStatus` page, clamped to 1-100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"Bing data, or `connected: false` with `authError` when there is no usable connection.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BingResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Ambiguous"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brands/{brand}/bing/submit":{"parameters":[{"$ref":"#/components/parameters/Brand"}],"post":{"operationId":"submitBrandBingUrls","summary":"Submit URLs to Bing for (re)crawl","description":"Submits live URLs to Bing for crawling through the brand's existing Bing Webmaster connection (the IndexNow equivalent, no key files). Pair it with `indexStatus` from `GET /brands/{brand}/bing`: find `indexed: false` URLs, then submit them here.\n\n- Only well-formed http(s) URLs are sent; malformed entries are reported in `errors`, exact duplicates are dropped silently. At most 500 valid URLs per request.\n- Respects Bing's remaining daily submission quota: URLs beyond it are not sent and come back in `errors` as `daily submit quota exhausted`. `quotaRemaining` is omitted when Bing does not report a quota.\n- A partial send stays 200. If Bing refused the batch and nothing was submitted, the same body comes back with status 502.\n- No usable Bing connection returns 409 `cannot submit: <reason>`.","tags":["Bing Webmaster"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BingSubmitRequest"},"example":{"urls":["https://tesla.com/new-page","https://tesla.com/model-y"]}}}},"responses":{"200":{"description":"URLs were submitted (possibly a quota-trimmed subset).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BingSubmitResponse"}}}},"400":{"description":"`body must be { urls: string[] }`, `no valid http(s) urls to submit`, or `too many urls (max 500 per request)`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Either the brand identifier is ambiguous (`ambiguous brand, use id`) or the brand has no usable Bing connection (`cannot submit: ...` followed by the reason, for example `brand is not connected to Bing Webmaster`).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"cannot submit: brand is not connected to Bing Webmaster"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"description":"Bing refused the batch and nothing was submitted. Same body as 200 with `submitted: 0` and the Bing error on every URL in `errors`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BingSubmitResponse"}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Send `Authorization: Bearer <YOUR_API_KEY>` on every request. Keys are issued on request: email support@rankzero.io. Each key is scoped to one account and only reaches the brands that account owns or shares via its organization; there is no all-brands access and no OAuth flow. A missing, malformed or unknown key returns 401.\n\nRate limits are per account: 3000 requests per hour with a burst limit of 120 per minute (counted per server instance, so treat them as approximate). Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (ISO 8601 time the current window resets). A refused request returns 429 with `Retry-After` (seconds) as well."}},"headers":{"RateLimitLimit":{"description":"The hourly request budget (3000) on a successful response. A 429 refused by the per-minute burst limit reports 120 instead.","schema":{"type":"integer"}},"RateLimitRemaining":{"description":"Requests left in the current window.","schema":{"type":"integer","minimum":0}},"RateLimitReset":{"description":"When the current window resets (ISO 8601 timestamp).","schema":{"type":"string","format":"date-time"}},"RetryAfter":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}}},"parameters":{"Brand":{"name":"brand","in":"path","required":true,"description":"The brand to address. Canonically the brand's `id` (a stable UUID from `GET /brands`). As a convenience the brand's domain or name also works, matched exactly and case-insensitively: one match resolves; several matches resolve only if exactly one of them is active, otherwise 409 `ambiguous brand, use id`; no match, or a brand outside the key's scope, is 404 `unknown brand`.","schema":{"type":"string"},"examples":{"id":{"summary":"Brand id (canonical)","value":"3f8a1c2e-9b4d-4e7a-bc1f-2d6e5a0b9c11"},"domain":{"summary":"Domain (convenience)","value":"tesla.com"}}},"Period":{"name":"period","in":"query","required":false,"description":"Reporting window. Unrecognized values fall back to `7d` rather than failing. `6m`, `12m` and `16m` (the Search Console retention ceiling) are meant for aggregate trends.","schema":{"$ref":"#/components/schemas/Period"}},"VisibilityTag":{"name":"tag","in":"query","required":false,"description":"Narrow AI visibility to active prompts carrying this tag. A tagged percentage is a share within that prompt set and is not summable across tags. Echoed back as `tag` (`null` when absent).","schema":{"type":"string"}},"CompareGsc":{"name":"compare","in":"query","required":false,"description":"Set to `previous` to add the same-length Search Console window immediately before the current one. Any other value is ignored.","schema":{"type":"string","enum":["previous"]}}},"responses":{"BadRequest":{"description":"Invalid query parameters or body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"query is required"}}}},"Unauthorized":{"description":"Missing or invalid API key.","headers":{"WWW-Authenticate":{"description":"RFC 6750 Bearer challenge carrying RFC 9728 `resource_metadata`, which points at the document that explains how to get a key. `error=\"invalid_token\"` is included only when a key was sent.","schema":{"type":"string","example":"Bearer realm=\"RankZero API\", error=\"invalid_token\", resource_metadata=\"https://www.rankzero.io/.well-known/oauth-protected-resource/api/v1\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unauthorized"}}}},"NotFound":{"description":"Unknown brand: no brand matches the identifier within the key's scope.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unknown brand"}}}},"Ambiguous":{"description":"The domain or name matched more than one brand. Retry with the brand id.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"ambiguous brand, use id"}}}},"AmbiguousOrPlanLimit":{"description":"Either the brand identifier is ambiguous (`ambiguous brand, use id`) or the write would exceed the plan's active-prompt limit (plan-limit body with the numbers). Nothing is written.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/PlanLimitError"},{"$ref":"#/components/schemas/Error"}]},"example":{"error":"Active prompt limit reached: 24 of 25 active on the starter plan, 6 more requested.","plan":"starter","limit":25,"current":24,"requested":6}}}},"RateLimited":{"description":"Rate limit exceeded for the key's account. Wait `Retry-After` seconds.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"rate limited"}}}},"InternalError":{"description":"Unexpected server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"internal error"}}}}},"schemas":{"Error":{"type":"object","description":"Flat error body.","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable message."}}},"PlanLimitError":{"type":"object","description":"A write refused because it would exceed the plan limit. Nothing was written.","required":["error","plan","limit","current","requested"],"properties":{"error":{"type":"string"},"plan":{"type":"string","description":"Plan id, for example `starter` or `professional`."},"limit":{"type":"integer","description":"The plan maximum."},"current":{"type":"integer","description":"Currently used."},"requested":{"type":"integer","description":"How many more this call would have used."}}},"Period":{"type":"string","description":"Reporting window: 24 hours, 7/30/90 days, or roughly 6, 12 and 16 months (183, 365 and 485 days).","enum":["24h","7d","30d","90d","6m","12m","16m"],"default":"7d"},"AiProvider":{"type":"string","description":"AI answer engine a prompt run was executed against.","enum":["chatgpt","gemini","perplexity","anthropic","google_ai_overview"]},"DateWindow":{"type":"object","description":"Inclusive calendar date range.","required":["startDate","endDate"],"properties":{"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"}}},"PlanBudget":{"type":"object","description":"Plan budget as it stands after the write.","required":["used","max","remaining"],"properties":{"used":{"type":"integer"},"max":{"type":"integer"},"remaining":{"type":"integer","minimum":0}}},"Brand":{"type":"object","required":["id","name","domain"],"properties":{"id":{"type":"string","format":"uuid","description":"Stable brand id; use it as `{brand}`."},"name":{"type":"string"},"domain":{"type":"string"}}},"Visibility":{"type":"object","description":"Own-brand AI visibility for the period.","required":["percent","rank"],"properties":{"percent":{"type":"number","description":"Share (0-100) of AI answers mentioning a tracked brand that mentioned this brand."},"rank":{"type":["integer","null"],"description":"Position in the industry ranking (1 = best)."}}},"RankingEntry":{"type":"object","required":["name","url","isOwnBrand","rank","visibilityPercent","sentiment"],"properties":{"name":{"type":"string"},"url":{"type":"string","description":"Bare domain."},"isOwnBrand":{"type":"boolean"},"rank":{"type":"integer","description":"1 = highest visibility."},"visibilityPercent":{"type":"number","description":"0-100."},"sentiment":{"type":"number","description":"Average sentiment of mentions, 0-100 (50 is neutral)."}}},"GscTotals":{"type":"object","description":"Search Console site totals.","required":["clicks","impressions","avgPosition","ctr"],"properties":{"clicks":{"type":"number"},"impressions":{"type":"number"},"avgPosition":{"type":"number"},"ctr":{"type":"number","description":"Fraction, 0-1."}}},"GscRow":{"type":"object","description":"One Search Console breakdown row: the requested dimension values plus metrics. With `compare=previous` it also carries the previous-window counterparts.","required":["clicks","impressions","ctr","position"],"properties":{"query":{"type":"string"},"page":{"type":"string"},"date":{"type":"string","format":"date"},"country":{"type":"string","description":"ISO 3166-1 alpha-3, lowercase."},"device":{"type":"string"},"searchAppearance":{"type":"string"},"clicks":{"type":"number"},"impressions":{"type":"number"},"ctr":{"type":"number","description":"Fraction, 0-1."},"position":{"type":"number"},"clicksPrev":{"type":"number","description":"Only with `compare=previous`. 0 when absent before."},"impressionsPrev":{"type":"number","description":"Only with `compare=previous`. 0 with current impressions > 0 marks an emerging query."},"positionPrev":{"type":"number","description":"Only with `compare=previous`. 0 means did not rank, not ranked first."}}},"Ga4SourceRow":{"type":"object","required":["source","sessions","aiSessions","ai","aiSource"],"properties":{"source":{"type":"string","description":"GA4 session source."},"sessions":{"type":"integer","description":"All sessions from it."},"aiSessions":{"type":"integer","description":"The part of those sessions that counts as AI search."},"ai":{"type":"boolean","description":"`aiSessions > 0`."},"aiSource":{"type":["string","null"],"description":"Assistant display name, for example `ChatGPT`."}}},"Ga4Summary":{"type":"object","description":"GA4 sessions by source, sorted by sessions descending.","required":["sessionsBySource","totalSessions","aiSessions"],"properties":{"sessionsBySource":{"type":"array","items":{"$ref":"#/components/schemas/Ga4SourceRow"}},"totalSessions":{"type":"integer"},"aiSessions":{"type":"integer"}}},"Ga4DailyRow":{"type":"object","required":["date","sessions","organicSessions","aiSessions"],"properties":{"date":{"type":"string","format":"date"},"sessions":{"type":"integer"},"organicSessions":{"type":"integer"},"aiSessions":{"type":"integer"}}},"Ga4LandingPageRow":{"type":"object","required":["landingPage","sessions","organicSessions","aiSessions"],"properties":{"landingPage":{"type":"string","description":"GA4 landing page path, without query string. `(not set)` when GA4 could not resolve one."},"sessions":{"type":"integer"},"organicSessions":{"type":"integer"},"aiSessions":{"type":"integer"}}},"Ga4EventRow":{"type":"object","required":["eventName","count","organicCount","aiCount"],"properties":{"eventName":{"type":"string"},"count":{"type":"integer"},"organicCount":{"type":"integer"},"aiCount":{"type":"integer"}}},"Ga4KeyEventRow":{"type":"object","description":"One key event with per-audience counts. `*Sessions` are sessions containing the event; `*SessionRate` is that over the audience total (0-1); revenue is ecommerce purchase revenue in the property currency.","required":["event","keyEvents","users","sessions","sessionRate","revenue","aiKeyEvents","aiUsers","aiSessions","aiSessionRate","aiRevenue","organicKeyEvents","organicUsers","organicSessions","organicSessionRate","organicRevenue"],"properties":{"event":{"type":"string"},"keyEvents":{"type":"number"},"users":{"type":"integer"},"sessions":{"type":"integer"},"sessionRate":{"type":"number"},"revenue":{"type":"number"},"aiKeyEvents":{"type":"number"},"aiUsers":{"type":"integer"},"aiSessions":{"type":"integer"},"aiSessionRate":{"type":"number"},"aiRevenue":{"type":"number"},"organicKeyEvents":{"type":"number"},"organicUsers":{"type":"integer"},"organicSessions":{"type":"integer"},"organicSessionRate":{"type":"number"},"organicRevenue":{"type":"number"}}},"Ga4Audience":{"type":"object","description":"An audience's totals. `sessionRate` is GA4's sessionKeyEventRate (share of sessions with any key event, 0-1). Revenue fields are 0 without ecommerce tracking.","required":["sessions","users","keyEvents","sessionRate","revenue","totalRevenue","transactions","revenuePerSession"],"properties":{"sessions":{"type":"integer"},"users":{"type":"integer"},"keyEvents":{"type":"number"},"sessionRate":{"type":"number"},"revenue":{"type":"number","description":"Ecommerce purchase revenue."},"totalRevenue":{"type":"number","description":"Purchase, subscription and ad revenue."},"transactions":{"type":"integer"},"revenuePerSession":{"type":"number"}}},"Ga4KeyEvents":{"type":"object","required":["rows","currency","all","ai","organic"],"properties":{"rows":{"type":"array","description":"Events marked as key events in GA4, sorted by keyEvents descending. Empty when none are marked.","items":{"$ref":"#/components/schemas/Ga4KeyEventRow"}},"currency":{"type":["string","null"],"description":"ISO 4217 code the revenue figures are in."},"all":{"$ref":"#/components/schemas/Ga4Audience"},"ai":{"$ref":"#/components/schemas/Ga4Audience"},"organic":{"$ref":"#/components/schemas/Ga4Audience"}}},"DirectAiEstimate":{"type":"object","description":"How much direct (unattributed) traffic could be AI with a stripped referrer, using Organic Search as a control. An estimate, never part of `aiSessions`.","required":["direct","homeOrUnknown","deep","rate","organicRate","excess","aiPages"],"properties":{"direct":{"type":"integer","description":"All direct sessions."},"homeOrUnknown":{"type":"integer","description":"Direct sessions on the root path or an unknown page."},"deep":{"type":"integer","description":"Direct sessions on a specific page."},"rate":{"type":"number","description":"Share (0-100) of `deep` landing on AI-cited pages."},"organicRate":{"type":"number","description":"The same share for Organic Search (the control)."},"excess":{"type":"integer","description":"Sessions by which `rate` outruns `organicRate`; 0 when the comparison cannot carry weight. The only number worth quoting."},"aiPages":{"type":"integer","description":"Distinct landing pages AI referrals used."}}},"Competitor":{"type":"object","required":["id","name","url","isOwnBrand","variations"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"url":{"type":"string","description":"Bare domain."},"isOwnBrand":{"type":"boolean"},"variations":{"type":"array","description":"Strings mention matching keys on.","items":{"type":"string"}}}},"CompetitorInput":{"type":"object","required":["name","url"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120,"description":"Competitor name. Mention matching is case-sensitive, so send the brand's own casing."},"url":{"type":"string","minLength":1,"description":"Anything domain-shaped (`https://www.rivian.com/r1s` or `rivian.com`); stored as the bare domain. Non-domains are refused."},"variations":{"type":["array","null"],"maxItems":20,"description":"Extra names added to the variations generated from `name`.","items":{"type":"string","minLength":1,"maxLength":80}},"isOwnBrand":{"type":"boolean","enum":[false],"description":"Cannot be set through the API; any value other than false is refused."}}},"CreateCompetitorsRequest":{"type":"object","required":["competitors"],"properties":{"competitors":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/CompetitorInput"}}}},"CompetitorItemError":{"type":"object","required":["error"],"properties":{"name":{"type":"string"},"url":{"type":"string"},"error":{"type":"string","description":"Why the item was refused, for example `already tracked for this brand` or `named more than once in this request`."},"existingCompetitorId":{"type":"string","format":"uuid","description":"The already-tracked competitor this item duplicates."}}},"CompetitorItemsError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/CompetitorItemError"}}}},"CitationSource":{"type":"object","required":["source_url","page_title","domain","usage_pct","avg_citations","prompt_texts","prompt_ids","content_type"],"properties":{"source_url":{"type":"string","description":"Normalized URL."},"page_title":{"type":["string","null"]},"domain":{"type":"string"},"usage_pct":{"type":"number","description":"Share (0-100) of successful runs citing this URL."},"avg_citations":{"type":"number","description":"Average times cited per citing answer."},"prompt_texts":{"type":"array","items":{"type":"string"}},"prompt_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"content_type":{"type":"string","description":"Content category, for example `Article`, `Docs`, `Review`, `News`, `Code`, `Page` or `Other`."}}},"MentionRow":{"type":"object","required":["prompt_id","run_id","prompt_text","model","output_preview","brands_mentioned_name","brands_mentioned_url","brands_mentioned_ids","run_at","country"],"properties":{"prompt_id":{"type":"string","format":"uuid"},"run_id":{"type":"string","format":"uuid"},"prompt_text":{"type":"string"},"model":{"$ref":"#/components/schemas/AiProvider"},"output_preview":{"type":["string","null"],"description":"Preview of the AI answer."},"brands_mentioned_name":{"type":"array","description":"Names of tracked competitors (own brand included) the answer mentioned, sorted by name.","items":{"type":"string"}},"brands_mentioned_url":{"type":"array","description":"Their domains, index-aligned with the names.","items":{"type":"string"}},"brands_mentioned_ids":{"type":"array","description":"Their competitor ids, index-aligned with the names.","items":{"type":"string","format":"uuid"}},"run_at":{"type":"string","format":"date-time"},"country":{"type":"string"}}},"PromptSource":{"type":"string","description":"Where a prompt came from. `suggestion_*` values mark prompts promoted from the suggestion queue.","enum":["manual","ai_generated","own_brand","suggestion_gsc","suggestion_bing","suggestion_paa","suggestion_generated","suggestion_rankzero","suggestion_perplexity"]},"Prompt":{"type":"object","required":["id","text","country","isActive","source","intents","tags","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"text":{"type":"string"},"country":{"type":"string","description":"Full country name, for example `United States`."},"isActive":{"type":"boolean"},"source":{"$ref":"#/components/schemas/PromptSource"},"intents":{"type":["array","null"],"description":"Intent classification, for example `commercial` or `informational`.","items":{"type":"string"}},"tags":{"type":["array","null"],"items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"}}},"PromptInput":{"type":"object","required":["text"],"properties":{"text":{"type":"string","minLength":1,"maxLength":500},"country":{"type":"string","minLength":1,"description":"A recognizable country (code or name). Defaults to the brand's own country."},"tags":{"type":["array","null"],"maxItems":20,"items":{"type":"string","minLength":1,"maxLength":60}},"isActive":{"type":"boolean","default":true}}},"CreatePromptsRequest":{"type":"object","required":["prompts"],"properties":{"prompts":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/PromptInput"}}}},"UpdatePromptsStatusRequest":{"type":"object","description":"Bulk activate or deactivate.","required":["promptIds","isActive"],"properties":{"promptIds":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","minLength":1}},"isActive":{"type":"boolean"}}},"PromptUpdateInput":{"type":"object","description":"At least one of `isActive`, `country`, `tags`, `addTags` or `removeTags` is required. `tags` cannot be combined with `addTags`/`removeTags`.","required":["id"],"properties":{"id":{"type":"string","minLength":1},"isActive":{"type":"boolean"},"country":{"type":"string","minLength":1,"description":"A recognizable country (code or name)."},"tags":{"type":"array","maxItems":20,"description":"Replaces the tag set. `[]` clears every tag.","items":{"type":"string","minLength":1,"maxLength":60}},"addTags":{"type":"array","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":60}},"removeTags":{"type":"array","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":60}}}},"UpdatePromptsEditsRequest":{"type":"object","description":"Per-prompt edits.","required":["updates"],"properties":{"updates":{"type":"array","minItems":1,"maxItems":100,"items":{"$ref":"#/components/schemas/PromptUpdateInput"}}}},"PromptItemError":{"type":"object","required":["error"],"properties":{"text":{"type":"string","description":"Identifies a refused create."},"id":{"type":"string","description":"Identifies a refused update."},"error":{"type":"string","description":"Why the item was refused, for example `already tracked for this brand and country` or `unknown prompt for this brand`."},"existingPromptId":{"type":"string","format":"uuid","description":"The existing prompt a create duplicates (PATCH it instead)."}}},"PromptItemsError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/PromptItemError"}}}},"PromptSuggestion":{"type":"object","required":["id","promptText","suggestionType","status","anchorQuery","confidenceScore","sourceQuery","country","source","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"promptText":{"type":"string"},"suggestionType":{"type":"string","description":"Origin, for example `direct_conversion_gsc`, `direct_conversion_bing`, `related_keywords_bing`, `keyphrase_expansion_paa`, `perplexity_related_question`."},"status":{"type":"string","description":"For example `suggested`, `accepted` or `rejected`."},"anchorQuery":{"type":["string","null"],"description":"The anchor query a variation belongs to."},"confidenceScore":{"type":"number"},"sourceQuery":{"type":["string","null"]},"country":{"type":["string","null"]},"source":{"type":"object","description":"Search demand behind the suggestion, where reported.","required":["clicks","impressions","ctr","position"],"properties":{"clicks":{"type":["number","null"]},"impressions":{"type":["number","null"]},"ctr":{"type":["number","null"]},"position":{"type":["number","null"]}}},"createdAt":{"type":"string","format":"date-time"}}},"BingAuthError":{"type":"object","description":"Why the brand has no usable Bing connection.","required":["code","message"],"properties":{"code":{"type":"string","enum":["not_connected","no_site_selected","not_configured","no_refresh_token","refresh_rejected","refresh_unreachable"]},"message":{"type":"string"}}},"BingQueryRow":{"type":"object","required":["query","clicks","impressions","ctr","position"],"properties":{"query":{"type":"string"},"clicks":{"type":"number"},"impressions":{"type":"number"},"ctr":{"type":"number","description":"Fraction, 0-1."},"position":{"type":"number","description":"Average position weighted by impressions."}}},"BingRelatedKeyword":{"type":"object","required":["query","impressions"],"properties":{"query":{"type":"string"},"impressions":{"type":"number","description":"Bing impression volume over the trailing 30 days."}}},"BingCrawlStats":{"type":"object","required":["inIndex","crawledPages","code2xx","code4xx","code5xx","blockedByRobotsTxt","containsMalware","crawlErrors","date"],"properties":{"inIndex":{"type":"number","description":"Pages in Bing's index (aggregate)."},"crawledPages":{"type":"number"},"code2xx":{"type":"number"},"code4xx":{"type":"number"},"code5xx":{"type":"number"},"blockedByRobotsTxt":{"type":"number"},"containsMalware":{"type":"number"},"crawlErrors":{"type":"number"},"date":{"type":["string","null"],"format":"date-time"}}},"BingCrawlIssue":{"type":"object","required":["url","label","severity"],"properties":{"url":{"type":"string"},"label":{"type":"string","description":"For example `Client error (4xx)` or `Redirect (301)`."},"severity":{"type":"string","enum":["error","warning"]}}},"BingIndexUrl":{"type":"object","required":["url","indexed"],"properties":{"url":{"type":"string"},"indexed":{"type":["boolean","null"],"description":"`true` crawled and indexed, `false` confidently not indexed, `null` unknown (lookup throttled or failed)."},"lastCrawl":{"type":"string","format":"date-time","description":"Present when `indexed` is true."},"reason":{"type":"string","description":"Present when `indexed` is false or null: `Not in Bing index`, `Discovered, not yet crawled`, or a `lookup throttled ...` / `lookup failed ...` message."}}},"BingIndexStatus":{"type":"object","required":["asOf","page","pageSize","count","indexedCount","notIndexedCount","unknownCount","urls"],"properties":{"asOf":{"type":"string","format":"date-time"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"count":{"type":"integer","description":"Total sitemap URLs (for paging)."},"indexedCount":{"type":"integer","description":"Indexed URLs on this page."},"notIndexedCount":{"type":"integer","description":"Not-indexed URLs on this page."},"unknownCount":{"type":"integer","description":"URLs on this page whose lookup was inconclusive."},"urls":{"type":"array","items":{"$ref":"#/components/schemas/BingIndexUrl"}}}},"BingSubmitRequest":{"type":"object","required":["urls"],"properties":{"urls":{"type":"array","description":"http(s) URLs to submit. At most 500 valid URLs per request.","items":{"type":"string","format":"uri"}}}},"BingSubmitItemError":{"type":"object","required":["error"],"properties":{"url":{"type":"string"},"error":{"type":"string","description":"For example `not a valid URL`, `not an http(s) URL`, `daily submit quota exhausted`, or the Bing error message."}}},"SearchResult":{"type":"object","required":["id","url","type","content"],"properties":{"id":{"type":"string"},"url":{"type":"string","description":"Site-relative URL, for example `/docs/metrics/visibility`."},"type":{"type":"string","enum":["page","heading","text"]},"content":{"type":"string"},"breadcrumbs":{"type":"array","items":{"type":"string"}},"contentWithHighlights":{"type":"array","items":{"type":"object","required":["type","content"],"properties":{"type":{"type":"string","enum":["text"]},"content":{"type":"string"},"styles":{"type":"object","properties":{"highlight":{"type":"boolean"}}}}}}}},"SearchResponse":{"type":"object","required":["query","results"],"properties":{"query":{"type":"string","description":"The trimmed query."},"results":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"}}}},"KpisResponse":{"type":"object","required":["brand","period","tag","gscWindow","asOf","visibility","gsc","ga4"],"properties":{"brand":{"type":"string","description":"Brand display name."},"period":{"$ref":"#/components/schemas/Period"},"tag":{"type":["string","null"],"description":"The visibility tag filter, `null` when none."},"gscWindow":{"description":"Range the Search Console figures cover (ends on the last settled day). `null` when GSC is unavailable.","oneOf":[{"$ref":"#/components/schemas/DateWindow"},{"type":"null"}]},"comparePeriod":{"description":"Only with `compare=previous`: the preceding Search Console window.","oneOf":[{"$ref":"#/components/schemas/DateWindow"},{"type":"null"}]},"asOf":{"type":"string","format":"date-time"},"visibility":{"description":"`null` when the ranking has no own-brand row.","oneOf":[{"$ref":"#/components/schemas/Visibility"},{"type":"null"}]},"gsc":{"description":"`null` when Search Console is not connected.","oneOf":[{"$ref":"#/components/schemas/GscTotals"},{"type":"null"}]},"gscPrev":{"description":"Only with `compare=previous`.","oneOf":[{"$ref":"#/components/schemas/GscTotals"},{"type":"null"}]},"ga4":{"description":"Sessions by source for `period` ending today. `null` when GA4 is not connected.","oneOf":[{"$ref":"#/components/schemas/Ga4Summary"},{"type":"null"}]}}},"OverviewResponse":{"type":"object","required":["brand","period","tag","asOf","visibility","ranking"],"properties":{"brand":{"type":"string","description":"Brand display name."},"period":{"$ref":"#/components/schemas/Period"},"tag":{"type":["string","null"]},"asOf":{"type":"string","format":"date-time"},"visibility":{"oneOf":[{"$ref":"#/components/schemas/Visibility"},{"type":"null"}]},"ranking":{"type":"array","description":"All tracked competitors, own brand included, by rank.","items":{"$ref":"#/components/schemas/RankingEntry"}}}},"GscResponse":{"type":"object","description":"Totals responses carry `gsc` (and `gscPrev` with compare); breakdown responses carry `dimensions` and `rows` instead.","required":["brand","period","window","asOf"],"properties":{"brand":{"type":"string","description":"Brand display name."},"period":{"$ref":"#/components/schemas/Period"},"window":{"description":"Range the numbers cover, ending on the last settled Search Console day. `null` when GSC is unavailable.","oneOf":[{"$ref":"#/components/schemas/DateWindow"},{"type":"null"}]},"comparePeriod":{"description":"Only with `compare=previous`.","oneOf":[{"$ref":"#/components/schemas/DateWindow"},{"type":"null"}]},"asOf":{"type":"string","format":"date-time"},"gsc":{"description":"Totals (no `dimensions`).","oneOf":[{"$ref":"#/components/schemas/GscTotals"},{"type":"null"}]},"gscPrev":{"description":"Previous-window totals (no `dimensions`, `compare=previous`).","oneOf":[{"$ref":"#/components/schemas/GscTotals"},{"type":"null"}]},"dimensions":{"type":"array","description":"The valid dimensions applied (breakdown only).","items":{"type":"string","enum":["query","page","date","country","device","searchAppearance"]}},"rows":{"type":["array","null"],"description":"Breakdown rows (breakdown only). `null` when GSC is unavailable.","items":{"$ref":"#/components/schemas/GscRow"}}}},"Ga4Response":{"type":"object","description":"Without `dimensions` the response carries `ga4`; with `dimensions` it carries `dimensions` and `rows` instead. `keyEvents` and `directEstimate` appear only when requested.","required":["brand","period","window","asOf"],"properties":{"brand":{"type":"string","description":"Brand display name."},"period":{"description":"`null` when an explicit `from`/`to` range was used.","oneOf":[{"$ref":"#/components/schemas/Period"},{"type":"null"}]},"window":{"$ref":"#/components/schemas/DateWindow"},"asOf":{"type":"string","format":"date-time"},"ga4":{"description":"Sessions by source (no `dimensions`).","oneOf":[{"$ref":"#/components/schemas/Ga4Summary"},{"type":"null"}]},"dimensions":{"type":"array","minItems":1,"maxItems":1,"items":{"type":"string","enum":["date","eventName","landingPage"]}},"rows":{"type":["array","null"],"description":"Daily rows for `dimensions=date`, event rows for `dimensions=eventName` or landing page rows for `dimensions=landingPage`. `null` when GA4 is unavailable.","items":{"anyOf":[{"$ref":"#/components/schemas/Ga4DailyRow"},{"$ref":"#/components/schemas/Ga4EventRow"},{"$ref":"#/components/schemas/Ga4LandingPageRow"}]}},"truncated":{"type":["boolean","null"],"description":"Only with `dimensions=landingPage`. `true` when a report hit GA4's row cap, so a 0 in a row is not a measured zero. `null` when GA4 is unavailable."},"keyEvents":{"description":"Only with `keyEvents=true`.","oneOf":[{"$ref":"#/components/schemas/Ga4KeyEvents"},{"type":"null"}]},"directEstimate":{"description":"Only with `directEstimate=true`.","oneOf":[{"$ref":"#/components/schemas/DirectAiEstimate"},{"type":"null"}]}}},"CompetitorsResponse":{"type":"object","required":["brand","asOf","competitors"],"properties":{"brand":{"type":"string","description":"Brand display name."},"asOf":{"type":"string","format":"date-time"},"competitors":{"type":"array","items":{"$ref":"#/components/schemas/Competitor"}}}},"CreateCompetitorsResponse":{"type":"object","required":["brand","asOf","created","competitors","trackedCompetitors","errors"],"properties":{"brand":{"type":"string","description":"Brand display name."},"asOf":{"type":"string","format":"date-time"},"created":{"type":"integer","minimum":0},"competitors":{"type":"array","description":"The competitors created by this call.","items":{"$ref":"#/components/schemas/Competitor"}},"trackedCompetitors":{"$ref":"#/components/schemas/PlanBudget"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/CompetitorItemError"}}}},"CitationsResponse":{"type":"object","required":["brand","period","asOf","sources"],"properties":{"brand":{"type":"string","description":"Brand display name."},"period":{"$ref":"#/components/schemas/Period"},"asOf":{"type":"string","format":"date-time"},"sources":{"type":"array","items":{"$ref":"#/components/schemas/CitationSource"}}}},"MentionsResponse":{"type":"object","required":["brand","period","window","asOf","page","pageSize","count","mentions"],"properties":{"brand":{"type":"string","description":"Brand display name."},"period":{"$ref":"#/components/schemas/Period"},"window":{"type":"object","required":["from","to"],"properties":{"from":{"type":["string","null"],"format":"date-time"},"to":{"type":"string","format":"date-time"}}},"asOf":{"type":"string","format":"date-time"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"count":{"type":"integer","description":"Total runs in the period, across all pages."},"mentions":{"type":"array","items":{"$ref":"#/components/schemas/MentionRow"}}}},"PromptsListResponse":{"type":"object","required":["brand","asOf","page","pageSize","count","prompts"],"properties":{"brand":{"type":"string","description":"Brand display name."},"asOf":{"type":"string","format":"date-time"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"count":{"type":"integer","description":"Total matching prompts, across all pages."},"prompts":{"type":"array","items":{"$ref":"#/components/schemas/Prompt"}}}},"CreatePromptsResponse":{"type":"object","required":["brand","asOf","created","prompts","activePrompts","errors"],"properties":{"brand":{"type":"string","description":"Brand display name."},"asOf":{"type":"string","format":"date-time"},"created":{"type":"integer","minimum":0},"prompts":{"type":"array","description":"The prompts created by this call.","items":{"$ref":"#/components/schemas/Prompt"}},"activePrompts":{"$ref":"#/components/schemas/PlanBudget"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/PromptItemError"}}}},"UpdatePromptsResponse":{"type":"object","required":["brand","asOf","updated","prompts","activePrompts","errors"],"properties":{"brand":{"type":"string","description":"Brand display name."},"asOf":{"type":"string","format":"date-time"},"updated":{"type":"integer","minimum":0},"prompts":{"type":"array","description":"The prompts as written.","items":{"$ref":"#/components/schemas/Prompt"}},"activePrompts":{"$ref":"#/components/schemas/PlanBudget"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/PromptItemError"}}}},"PromptSuggestionsResponse":{"type":"object","required":["brand","asOf","count","suggestions"],"properties":{"brand":{"type":"string","description":"Brand display name."},"asOf":{"type":"string","format":"date-time"},"count":{"type":"integer"},"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/PromptSuggestion"}}}},"BingResponse":{"type":"object","required":["brand","asOf","connected","siteUrl","authError","seed","queries","related","crawlStats","crawlIssues","indexStatus"],"properties":{"brand":{"type":"string","description":"Brand display name."},"asOf":{"type":"string","format":"date-time"},"connected":{"type":"boolean","description":"Whether a usable Bing credential was resolved live."},"siteUrl":{"type":["string","null"],"description":"The Bing Webmaster site the calls were made against. Reported even when auth fails, when known."},"authError":{"description":"`null` when connected.","oneOf":[{"$ref":"#/components/schemas/BingAuthError"},{"type":"null"}]},"seed":{"type":["string","null"]},"queries":{"type":"array","items":{"$ref":"#/components/schemas/BingQueryRow"}},"related":{"type":"array","items":{"$ref":"#/components/schemas/BingRelatedKeyword"}},"crawlStats":{"oneOf":[{"$ref":"#/components/schemas/BingCrawlStats"},{"type":"null"}]},"crawlIssues":{"type":"array","items":{"$ref":"#/components/schemas/BingCrawlIssue"}},"indexStatus":{"description":"`null` when not connected.","oneOf":[{"$ref":"#/components/schemas/BingIndexStatus"},{"type":"null"}]}}},"AuditIndexState":{"type":"object","required":["state","reason","checkedAt"],"properties":{"state":{"type":"string","enum":["indexed","not_indexed","crowded_out","not_inspected"]},"reason":{"type":["string","null"],"description":"The coverage state when `not_indexed`, for example `Discovered - currently not indexed`."},"checkedAt":{"type":["string","null"],"format":"date-time","description":"When the inspection run this state comes from ran."}}},"AuditTodo":{"type":"object","required":["level","category","issue","severity","source","url","urls","group","details","auditedAt","audits","createdAt"],"properties":{"level":{"type":"string","enum":["site","page"]},"category":{"type":"string","enum":["Content","Images","Social","Technical","Sitewide","Indexing","Crawl","Performance"]},"issue":{"type":"string","description":"The to-do, e.g. `Fix page returning 4XX`."},"severity":{"type":"string","enum":["error","warning","info"]},"source":{"type":"string","enum":["crawler","gsc","bing"]},"url":{"type":"string","description":"`Sitewide` for a site-level to-do; otherwise the page, or the first affected page of a group finding."},"urls":{"type":"array","items":{"type":"string"},"description":"Every affected page. Empty for a site-level to-do."},"group":{"description":"Set on a template finding reported for a group: `sampled` of its pages audited, `size` in the sitemap.","oneOf":[{"type":"object","required":["key","size","sampled"],"properties":{"key":{"type":"string","description":"For example `tesla.com/products/*`."},"size":{"type":"integer"},"sampled":{"type":"integer"}}},{"type":"null"}]},"details":{"type":["string","null"]},"auditedAt":{"type":["string","null"],"format":"date-time","description":"For a crawler page finding, when the audit (or, for a Core Web Vitals finding, the CrUX reading) it came from was taken; for a group finding, the oldest of its pages. `null` for site-level and Search Console or Bing to-dos. Compare it with the latest crawl before treating an absent finding as fixed."},"audits":{"type":"array","description":"Each affected page with the date of its audit. Empty for site-level and Search Console or Bing to-dos.","items":{"type":"object","required":["url","auditedAt"],"properties":{"url":{"type":"string"},"auditedAt":{"type":"string","format":"date-time"}}}},"createdAt":{"type":["string","null"],"format":"date-time","description":"When the audit, record or inspection behind it ran."}}},"AuditSite":{"type":"object","description":"The sitewide record of the newest crawl. JSON fields keep the shape they are stored in.","properties":{"checkedAt":{"type":"string","format":"date-time"},"homepageUrl":{"type":["string","null"]},"homepageStatus":{"type":["integer","null"]},"https":{"type":["boolean","null"]},"httpRedirectsToHttps":{"type":["boolean","null"],"description":"`null` when http:// did not answer."},"canonicalHost":{"type":["string","null"]},"hostRedirect":{"type":["string","null"],"enum":["to_www","to_apex","to_other","both_serve","apex_only","www_only","split","unreachable",null]},"hostChecks":{"type":["array","null"],"items":{"type":"object","properties":{"url":{"type":"string"},"status":{"type":["integer","null"]},"final_url":{"type":["string","null"]}}}},"robotsTxt":{"type":"object","properties":{"present":{"type":"boolean"},"status":{"type":["integer","null"]},"sitemaps":{"type":"array","items":{"type":"string"}}}},"aiCrawlers":{"type":["array","null"],"items":{"type":"object","properties":{"agent":{"type":"string"},"label":{"type":"string"},"allowed":{"type":"boolean"},"source":{"type":"string","enum":["explicit","wildcard","default"]}}}},"sitemap":{"type":"object","properties":{"present":{"type":"boolean"},"url":{"type":["string","null"]},"urlCount":{"type":["integer","null"]}}},"llmsTxt":{"type":"object","properties":{"present":{"type":"boolean"},"status":{"type":["integer","null"]}}},"favicon":{"type":"object","properties":{"url":{"type":["string","null"],"description":"The homepage's `<link rel=\"icon\">`."},"faviconIco":{"type":["boolean","null"]}}},"organization":{"type":["object","null"],"properties":{"types":{"type":"array","items":{"type":"string"}},"name":{"type":["string","null"]},"url":{"type":["string","null"]},"logo":{"type":"boolean"},"contactPoint":{"type":"boolean"},"address":{"type":"boolean"},"sameAs":{"type":"integer"}}},"website":{"type":["object","null"],"properties":{"name":{"type":["string","null"]},"url":{"type":["string","null"]},"searchAction":{"type":"boolean"}}},"structuredData":{"type":"object","properties":{"pages":{"type":["integer","null"]},"measured":{"type":"integer","description":"Crawled sitemap pages that answered 2XX."},"share":{"type":["number","null"]}}},"performance":{"type":"object","description":"Speed for tracked brands. `crux` is the origin's Chrome UX Report record (p75 of real Chrome users, all form factors), `null` until first read; tier `none` means neither the apex nor the www origin has field data. `lab` is the homepage's newest PageSpeed Insights run (Lighthouse, mobile), diagnosis rather than a finding. Only failing field data raises a to-do.","properties":{"thresholds":{"type":"object","properties":{"lcpMs":{"type":"integer","example":2500},"inpMs":{"type":"integer","example":200},"cls":{"type":"number","example":0.1}}},"crux":{"oneOf":[{"$ref":"#/components/schemas/AuditCruxReading"},{"type":"null"}]},"lab":{"oneOf":[{"$ref":"#/components/schemas/AuditLabRun"},{"type":"null"}]}}},"authority":{"type":"object","description":"Domain Rating from Ahrefs, read weekly for tracked brands. Show `domainRating` only with `attribution`, linked, which the Ahrefs licence requires. `domainRating` and `checkedAt` are `null` until the first reading.","required":["domainRating","attribution","checkedAt"],"properties":{"domainRating":{"type":["number","null"]},"attribution":{"type":"object","required":["text","url"],"properties":{"text":{"type":"string","const":"Domain Rating by Ahrefs"},"url":{"type":"string","const":"https://ahrefs.com/"}}},"checkedAt":{"type":["string","null"],"format":"date-time","description":"When Ahrefs gave the Domain Rating."}}}}},"AuditCruxReading":{"type":"object","required":["tier","lcpP75Ms","inpP75Ms","clsP75","checkedAt"],"properties":{"tier":{"type":"string","enum":["url","origin","none"],"description":"Which record answered: the page's own, its origin's, or none (every metric null, never zero)."},"origin":{"type":["string","null"]},"lcpP75Ms":{"type":["integer","null"]},"inpP75Ms":{"type":["integer","null"]},"clsP75":{"type":["number","null"]},"passes":{"type":"object","description":"Per metric, whether the p75 is within its threshold.","properties":{"lcp":{"type":["boolean","null"]},"inp":{"type":["boolean","null"]},"cls":{"type":["boolean","null"]}}},"checkedAt":{"type":["string","null"],"format":"date-time"}}},"AuditLabRun":{"type":"object","required":["performanceScore","lcpMs","cls","tbtMs","opportunities","checkedAt"],"properties":{"reason":{"type":"string","enum":["homepage","traffic","field_failure"]},"performanceScore":{"type":"number","description":"0 to 1."},"lcpMs":{"type":"integer"},"cls":{"type":"number"},"tbtMs":{"type":"integer"},"opportunities":{"type":"array","description":"The three largest estimated savings.","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"savingsMs":{"type":"integer"},"savingsBytes":{"type":["integer","null"]}}}},"checkedAt":{"type":"string","format":"date-time"}}},"AuditPage":{"type":"object","required":["url","audited","selection","template","index","issues"],"properties":{"url":{"type":"string"},"audited":{"type":"boolean"},"auditedAt":{"type":["string","null"],"format":"date-time"},"status":{"type":["integer","null"],"description":"The status the audit ended on after redirects. `null` for an audit stored before statuses were recorded, or a request that got no answer."},"finalUrl":{"type":["string","null"]},"redirected":{"type":["boolean","null"]},"noindex":{"type":["boolean","null"]},"canonical":{"type":["string","null"]},"title":{"type":["string","null"]},"titleCount":{"type":["integer","null"]},"metaDescription":{"type":["string","null"]},"h1":{"type":["string","null"]},"h1Count":{"type":["integer","null"]},"wordCount":{"type":["integer","null"]},"lang":{"type":["string","null"]},"hreflang":{"type":["array","null"],"items":{"type":"object","properties":{"lang":{"type":"string"},"href":{"type":"string"}}}},"internalLinks":{"type":["integer","null"]},"externalLinks":{"type":["integer","null"]},"imagesTotal":{"type":["integer","null"]},"imagesWithAlt":{"type":["integer","null"]},"hasOpenGraph":{"type":["boolean","null"]},"hasTwitterCards":{"type":["boolean","null"]},"hasJsonLd":{"type":["boolean","null"]},"structuredDataTypes":{"type":["array","null"],"items":{"type":"string"}},"template":{"description":"The template group, and how many sitemap URLs share it when that is over 50 (`null` for a smaller group). Over 50, the crawl samples it weekly and reports its template findings once for the group.","oneOf":[{"type":"object","properties":{"group":{"type":"string"},"size":{"type":["integer","null"]}}},{"type":"null"}]},"selection":{"description":"Why the crawl last selected this page, `null` when it has not yet.","oneOf":[{"type":"object","properties":{"reason":{"type":"string","enum":["recheck","changed","template","traffic","rotation"]},"selectedAt":{"type":["string","null"],"format":"date-time"}}},{"type":"null"}]},"lastmod":{"type":["string","null"],"format":"date-time","description":"The sitemap's `<lastmod>`."},"firstSeenAt":{"type":["string","null"],"format":"date-time","description":"When a sitemap read first listed the page."},"index":{"type":"object","properties":{"google":{"oneOf":[{"$ref":"#/components/schemas/AuditIndexState"},{"type":"null"}]},"bing":{"oneOf":[{"$ref":"#/components/schemas/AuditIndexState"},{"type":"null"}]}}},"crux":{"description":"The page's newest Chrome UX Report reading with the tier that answered, for tracked brands; `null` before its first reading.","oneOf":[{"$ref":"#/components/schemas/AuditCruxReading"},{"type":"null"}]},"lab":{"description":"The page's newest PageSpeed Insights run (Lighthouse, mobile), taken for the homepage, the top pages by clicks and pages whose own field record fails; `null` otherwise.","oneOf":[{"$ref":"#/components/schemas/AuditLabRun"},{"type":"null"}]},"images":{"type":"object","properties":{"total":{"type":["integer","null"]},"bands":{"type":"object","description":"An image over `needsImprovementBytes` needs improvement (orange); over `poorBytes` it is poor (red). Under the first is fine.","properties":{"needsImprovementBytes":{"type":"integer","example":200000},"poorBytes":{"type":"integer","example":500000}}},"oversized":{"type":"array","description":"Images the page references over `bands.needsImprovementBytes`, heaviest first. Empty when none, or before their sizes are checked.","items":{"type":"object","required":["url","bytes","rating"],"properties":{"url":{"type":"string"},"bytes":{"type":"integer"},"rating":{"type":"string","enum":["needs-improvement","poor"]}}}}}},"issues":{"type":"array","items":{"type":"string"},"description":"The page-level to-dos that name this page."}}},"AuditResponse":{"type":"object","required":["brand","checkedAt","sitemapTotal","total","index","selection","site","todos","todosTruncated","pages","limit","nextCursor"],"properties":{"brand":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string"}}},"checkedAt":{"type":["string","null"],"format":"date-time","description":"When the last Search Console inspection run ran. `null` without Search Console."},"sitemapTotal":{"type":"integer","description":"URLs the sitemap lists."},"total":{"type":["integer","null"],"description":"URLs the last Search Console inspection run checked. Fewer than `sitemapTotal` means partial inspection."},"index":{"type":"object","properties":{"google":{"type":["object","null"],"properties":{"indexed":{"type":["integer","null"]},"total":{"type":["integer","null"]},"notIndexed":{"type":["integer","null"]},"notIndexedNamed":{"type":"integer","description":"Not-indexed pages the cached issues name. Below `notIndexed`, inspected pages are `crowded_out` rather than `indexed`."},"checkedAt":{"type":["string","null"],"format":"date-time"}}},"bing":{"type":["object","null"],"properties":{"indexed":{"type":["integer","null"]},"checkedAt":{"type":["string","null"],"format":"date-time"}}}}},"selection":{"type":["object","null"],"description":"What the last weekly crawl was built from: `at`, `budget`, `inventory`, `complete`, `childSitemaps`, `skipped`, `counts` per reason and the sampled `groups`."},"site":{"oneOf":[{"$ref":"#/components/schemas/AuditSite"},{"type":"null"}]},"todos":{"type":"array","items":{"$ref":"#/components/schemas/AuditTodo"}},"todosTruncated":{"type":"boolean","description":"True when the site has more audited sitemap pages than the 20,000 the to-dos read covers."},"pages":{"type":"array","items":{"$ref":"#/components/schemas/AuditPage"}},"limit":{"type":"integer"},"nextCursor":{"type":["string","null"],"description":"Pass as `cursor` for the next page; `null` on the last."}}},"BingSubmitResponse":{"type":"object","required":["brand","siteUrl","asOf","submitted","errors"],"properties":{"brand":{"type":"string","description":"Brand display name."},"siteUrl":{"type":"string"},"asOf":{"type":"string","format":"date-time"},"submitted":{"type":"integer","minimum":0,"description":"URLs actually sent to Bing."},"quotaRemaining":{"type":"integer","minimum":0,"description":"Daily submission quota left after this call. Omitted when Bing does not report a quota."},"errors":{"type":"array","items":{"$ref":"#/components/schemas/BingSubmitItemError"}}}}}}}