{"openapi":"3.1.0","info":{"title":"Shruwd API","version":"1","summary":"Measure how a brand appears in AI answers, diagnose why, and prove movement.","description":"Shruwd tracks how a brand and its competitors show up in AI-generated answers\n(Google AI Overviews and ChatGPT), sets that beside first-party AI-crawler\nlogs, diagnoses **why** the brand is or is not cited, and re-checks\nwhether a fix moved anything.\n\n**Authentication.** `Authorization: Bearer sh_live_…`. Keys are minted in the\ndashboard and carry scopes: `read` (every GET), `write` (everything else), or\n`default` (both). A dashboard session works too.\n\n**Tenant.** A key works on the one workspace it was minted for. A dashboard\nsession works on the workspace named by the `X-Shruwd-Workspace` header, else\nthe caller's own; a key ignores the header. A brand, prompt, entity or finding\nthat belongs to another workspace is a `404`, indistinguishable from one that\ndoes not exist.\n\n**Roles.** A request acts with its caller's role in the workspace: reads need\n`viewer`, writes `editor`; creating or archiving a brand needs `owner`, and\nminting an ingest token `admin`. A key never exceeds its holder's role.\nRefusals are `403 not_a_member` and `403 insufficient_role`.\n\n**Errors.** The body is always `{ code, message, retryable, details? }` with the\nHTTP status. `retryable: true` (429, 5xx) may be retried with backoff;\n`Retry-After` is set on 429.\n\n**Rate limits.** Per key, per minute: 120 GET, 30 for everything else.\n\n**Metrics.** Every metric is a `DisplayableMetric`: a point estimate with its\n95% interval and `n`, or `insufficient_data` when fewer than ten responses\nexist, or `undefined` when the denominator is zero. `insufficient_data` is\n**not** zero. Do not report it as a number.\n\n**Measurement is asynchronous.** Creating a brand or a cycle schedules work;\nresults arrive over the following hours and are read from `visibility`,\n`crawlers` and `findings`.\n","contact":{"name":"Shruwd","url":"https://shruwd.io"},"x-logo":{"url":"https://shruwd.io/android-chrome-192x192.png"}},"servers":[{"url":"https://shruwd.io/api/v1"}],"security":[{"apiKey":[]}],"tags":[{"name":"workspace","description":"Plan, entitlements, usage."},{"name":"brands","description":"A brand is the thing measured — a name, a domain, and its competitors."},{"name":"prompts","description":"The fixed questions asked of each engine every cycle."},{"name":"entities","description":"The brand itself and its competitors, with the exact aliases that count as a mention."},{"name":"measurement","description":"Cycles, visibility metrics, crawler activity."},{"name":"findings","description":"Diagnoses with a specific recommended fix, and the recheck that proves movement."},{"name":"connections","description":"First-party data sources."},{"name":"discovery","description":"This document."}],"paths":{"/workspace":{"get":{"operationId":"getWorkspace","tags":["workspace"],"summary":"The caller's workspace","description":"Plan, resolved entitlements (plan plus any one-off grant — what the\nscheduler applies), usage against the period allowance, and brands. Call\nthis first: it says what the plan will let you do before you try.\n","responses":{"200":{"description":"The workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/brands":{"get":{"operationId":"listBrands","tags":["brands"],"summary":"List brands","responses":{"200":{"description":"Active brands.","content":{"application/json":{"schema":{"type":"object","required":["brands"],"properties":{"brands":{"type":"array","items":{"$ref":"#/components/schemas/BrandSummary"}}}}}}},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"createBrand","tags":["brands"],"summary":"Create a brand","description":"Creates the brand and its **self entity** with one alias (the name) and\none domain (the registrable domain of `domain`), then plants the first\ncycle so the next sweep measures it.\n\nA caller with no workspace gets one here, on the free tier.\n\nOn the free tier the first cycle is the one free measurement, allowed\nonce per registrable domain ever; `snapshot` says whether it was\n`planned` or `already_taken`. On a weekly plan the first cycle is a\n`scheduled` one due now and `snapshot` is `null`.\n\nA name of six characters or fewer, or an ordinary English word, is\nrefused with `422 context_terms_required` until `contextTerms` is\ngiven — the same rule as a competitor, because the brand's name is\nits own first alias and the headline mention rate counts it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBrandRequest"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandCreated"}}}},"409":{"description":"`duplicate_domain`: an unarchived brand in this workspace already has the domain (`details` names it). `plan_limit`: the plan's brand count is used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`context_terms_required`, `invalid_domain`, `invalid_name`, `invalid_timezone`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"getBrand","tags":["brands"],"summary":"One brand, with its pipeline health","description":"Counts, the engines the plan measures, and the health block: last\nsuccessful cycle, next scheduled cycle, run success rate, log-ingest\ncontinuity, connection states. Read this before treating a flat metric\nas a signal.\n","responses":{"200":{"description":"The brand.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandDetail"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}},"patch":{"operationId":"updateBrand","tags":["brands"],"summary":"Rename a brand or change its timezone","description":"`name` is the display name; matching uses the self entity's aliases,\nedited on the entity. `timezone` moves the day boundary for cycles and\nrollups from the next cycle on; history is not recomputed. The domain\ncannot change — it defines the self entity — so a new domain is a new\nbrand.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBrandRequest"}}}},"responses":{"200":{"description":"The brand.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandSummary"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"archiveBrand","tags":["brands"],"summary":"Archive a brand","description":"Nothing is deleted. Scheduling stops; history stays.","responses":{"200":{"description":"Archived.","content":{"application/json":{"schema":{"type":"object","required":["brandId","archived"],"properties":{"brandId":{"type":"string"},"archived":{"type":"boolean","const":true}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/prompts":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"listPrompts","tags":["prompts"],"summary":"List prompts","description":"Active prompts. With `engine`, each prompt carries its `coverage` over\nthe window from the rollups. Per-prompt coverage is often below n = 10;\nthe metric says `insufficient_data` rather than showing a number.\n","parameters":[{"name":"includeInactive","in":"query","schema":{"type":"string","enum":["1"]},"description":"Also return deactivated prompts."},{"$ref":"#/components/parameters/engineOptional"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"}],"responses":{"200":{"description":"Prompts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptList"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"addPrompts","tags":["prompts"],"summary":"Add prompts","description":"Batch, atomic. `intent` is required: it decides which diagnostic rules\napply. Text is 1–500 characters (the tighter of the two engines' caps).\nA duplicate of an active prompt is `409 duplicate_prompt`. If the batch\nwould exceed the plan's prompt allowance — one pool across every brand in\nthe workspace, not a per-brand limit — nothing is created and\n`409 plan_limit` carries `limit`, `current` and `submitted`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddPromptsRequest"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","required":["prompts"],"properties":{"prompts":{"type":"array","items":{"$ref":"#/components/schemas/Prompt"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`plan_limit` or `duplicate_prompt`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`invalid_prompt`, `invalid_intent`, `invalid_tags`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/prompts/{promptGroupId}":{"parameters":[{"$ref":"#/components/parameters/promptGroupId"}],"patch":{"operationId":"updatePrompt","tags":["prompts"],"summary":"Edit a prompt","description":"A change to `text` or `intent` creates a new version under the same\n`promptGroupId` (new `promptId`, `version` incremented); runs already\nexecuted keep pointing at the version that produced them. `tags` and\n`active` change in place. Reactivating counts as a creation against the\nplan limit.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptPatch"}}}},"responses":{"200":{"description":"The current version.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Prompt"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PlanLimit"},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"removePrompt","tags":["prompts"],"summary":"Deactivate a prompt","description":"Deactivates. Never deletes; the history it produced stays interpretable.","responses":{"200":{"description":"Deactivated.","content":{"application/json":{"schema":{"type":"object","required":["promptGroupId","active"],"properties":{"promptGroupId":{"type":"string"},"active":{"type":"boolean","const":false}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/entities":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"listEntities","tags":["entities"],"summary":"List entities","description":"The self entity first, then every competitor, each with its current aliases, domains, exclusions and context terms.","responses":{"200":{"description":"Entities.","content":{"application/json":{"schema":{"type":"object","required":["entities"],"properties":{"entities":{"type":"array","items":{"$ref":"#/components/schemas/Entity"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"addEntity","tags":["entities"],"summary":"Add a competitor","description":"Competitors only; the self entity comes with the brand.\n\nAliases are exact strings matched on token boundaries — no patterns, no\nfuzzy matching. `aliases` defaults to the name.\n\n**Short or common-word names need context terms.** A name of six\ncharacters or fewer, or an ordinary English word (\"Arc\", \"Linear\",\n\"Notion\"), matches sentences that are not about the company. Such a name\nis refused with `422 context_terms_required` until `contextTerms` is\ngiven; a mention then counts only when one of the terms appears nearby.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEntityRequest"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entity"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`domain_taken`: another entity of this brand owns the domain.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`context_terms_required`, `invalid_alias`, `invalid_domain`, `invalid_name`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/entities/{entityId}":{"parameters":[{"$ref":"#/components/parameters/entityId"}],"put":{"operationId":"setEntity","tags":["entities"],"summary":"Set an entity's aliases, domains, exclusions and context terms","description":"The **desired** state. The server diffs against the current rows: what\nis gone is closed as of now, what is new is added, what is unchanged is\nuntouched. A run executed yesterday still resolves against yesterday's\naliases. Works for the self entity too. A generic name cannot have its\ncontext terms removed.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityConfig"}}}},"responses":{"200":{"description":"The entity as it now is.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entity"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`domain_taken`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`context_terms_required`, `invalid_alias`, `invalid_domain`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"removeEntity","tags":["entities"],"summary":"Remove a competitor","description":"Closes the entity and its rows as of now. The self entity cannot be removed (`409 self_entity`); archive the brand instead.","responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","required":["entityId","removed"],"properties":{"entityId":{"type":"string"},"removed":{"type":"boolean","const":true}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`self_entity`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/cycles":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"listCycles","tags":["measurement"],"summary":"Recent cycles","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Cycles, newest first.","content":{"application/json":{"schema":{"type":"object","required":["cycles"],"properties":{"cycles":{"type":"array","items":{"$ref":"#/components/schemas/Cycle"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"runMeasurement","tags":["measurement"],"summary":"Measure now","description":"Creates a `manual` cycle due immediately. The scheduler picks it up\nwithin fifteen minutes and fans out one run per prompt × engine ×\nrepetition; results land over the following hours. This response does\nnot contain them — read `visibility` and `findings` later.\n\nA cycle spends the period's run allowance. On the free tier this is the\none free measurement per registrable domain; once taken it is\n`409 snapshot_taken`. A workspace past its billing grace period gets\n`402 workspace_not_active`.\n","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false}}}},"responses":{"201":{"description":"Cycle created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CycleCreated"}}}},"402":{"description":"`workspace_not_active`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`snapshot_taken`, or `workspace_on_hold` (measurement paused pending review; see `GET /workspace`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/visibility":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"getVisibility","tags":["measurement"],"summary":"Visibility metrics","description":"Mention rate and share of voice per tracked entity on one engine over a\nwindow, with per-prompt coverage. Read from precomputed rollups; every\nmetric is a `DisplayableMetric`. `from` is clamped to the plan's history\nwindow and `historyFrom` says when that happened. `asOf` is the day the\nnumbers describe when a closed cycle served them.\n","parameters":[{"$ref":"#/components/parameters/engine"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"}],"responses":{"200":{"description":"Visibility.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Visibility"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/visibility/series":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"getVisibilitySeries","tags":["measurement"],"summary":"Visibility over time","description":"The window metric at every cycle close in the range: one point per\n`asOf` day the period rollup wrote, for the brand and each competitor,\non one engine. Read from the period rollup only. Each point's metrics\nare `DisplayableMetric`s, so a close below ten responses is a gap in\nthe line, never a zero. `modelChanges` marks the closes where the\nprovider's model set differed from the previous close; a comparison\nacross one is confounded (`metrics.md` §6.3). `from` defaults to\nninety days back and is clamped to the plan's history window.\n","parameters":[{"$ref":"#/components/parameters/engine"},{"name":"window","in":"query","schema":{"type":"integer","enum":[7,30,90],"default":30},"description":"The rolling window each point describes, in days."},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"}],"responses":{"200":{"description":"Visibility series.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VisibilitySeries"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/crawlers":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"getCrawlers","tags":["measurement"],"summary":"AI-crawler activity","description":"Per-bot hits from the brand's server logs over the window, plus how\ncontinuous the log ingest has been. `verifiedHits` are hits whose source\nIP was confirmed against the vendor's published ranges; `unverifiedHits`\nmerely claimed the user-agent. **Only verified hits are a signal.** They\nare never summed, here or anywhere.\n","parameters":[{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"}],"responses":{"200":{"description":"Crawler activity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Crawlers"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/findings":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"listFindings","tags":["findings"],"summary":"List findings","description":"Ordered by severity, then confidence, then estimated impact — the same\norder the dashboard and the weekly digest use. By default the open,\nacknowledged, fix-applied and rechecking findings; `states` narrows or\nwidens that. Suppressed findings (a more fundamental finding explains\nthem) are hidden unless `suppressed=1`.\n\n`confidence`: `observed` means measured directly from first-party data;\n`inferred` means derived from a reliable pattern; `heuristic` means a\nplausible cause that could not be confirmed. Treat `heuristic` as a\nhypothesis to test, not a fact.\n","parameters":[{"name":"states","in":"query","schema":{"type":"string"},"description":"Comma-separated subset of `open,acknowledged,fix_applied,rechecking,resolved,not_moved,dismissed,stale`."},{"name":"suppressed","in":"query","schema":{"type":"string","enum":["1"]},"description":"Include suppressed findings, with `suppressedBy` set."}],"responses":{"200":{"description":"Findings.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindingList"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/findings/{findingId}":{"parameters":[{"$ref":"#/components/parameters/findingId"}],"get":{"operationId":"getFinding","tags":["findings"],"summary":"One finding with its evidence and history","responses":{"200":{"description":"The finding.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FindingDetail"}}}},"403":{"description":"`not_entitled`: the plan shows only its first `maxVisibleFindings` findings in full and this is not one of them — it is counted in a findings list's `locked`. `details` carries `plan`, `resource` and `limit`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/findings/{findingId}/transition":{"parameters":[{"$ref":"#/components/parameters/findingId"}],"post":{"operationId":"transitionFinding","tags":["findings"],"summary":"Acknowledge, mark a fix applied, or dismiss","description":"The human half of the lifecycle. `fix_applied` captures a baseline of\nthe affected prompts server-side and starts the recheck clock: fourteen\ndays later a recheck cycle measures again, and the finding becomes\n`resolved` or `not_moved` **only** if the movement gates pass —\nnon-overlapping confidence intervals and at least five points of change.\nNeither `resolved` nor `not_moved` can be set by a request.\n\nStructural findings — a refused AI crawler, content that needs\nJavaScript, a page never crawled — are confirmed from first-party data\ninstead: they need no prompt baseline, `recheck` is `null`, and the\nfinding becomes `resolved` as soon as the fix shows up in the logs or\non the page.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransitionRequest"}}}},"responses":{"200":{"description":"Moved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransitionResult"}}}},"400":{"description":"`invalid_target_state`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`not_entitled`: the plan does not show this finding in full, and a locked finding is neither shown nor moved. `details` carries `plan`, `resource` and `limit`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`invalid_transition` (not allowed from the current state) or `no_baseline_available` (no tracked question has cited the finding's subject, so there is nothing to measure a fix against; structural findings never get this).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/ingest-token":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"post":{"operationId":"createIngestToken","tags":["connections"],"summary":"Mint a log-drain token","description":"Returns the token **once**, with the endpoint to send server logs to\n(`POST <endpoint>` with `Authorization: Bearer <token>`, one log line\nper row). Previous tokens for the brand are revoked. Log ingest is what\npowers the crawler view and the highest-confidence findings.\n","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","maxLength":80,"default":"Log drain"}}}}}},"responses":{"201":{"description":"The token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngestToken"}}}},"403":{"description":"`not_entitled`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/suggestions":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"listSuggestions","tags":["entities"],"summary":"Names the answers use that you do not track","description":"The discovery pass: capitalised names found in the AI answers that match\nnone of the brand's aliases, surfaced when they appear in at least 20% of\na prompt cluster's responses. Each carries that share so the reason it is\nhere is visible. Accept one to make it a competitor (it then counts in\nshare of voice); dismiss one to quiet it for ninety days.\n","parameters":[{"name":"state","in":"query","schema":{"type":"string","enum":["open","accepted","dismissed","all"],"default":"open"}}],"responses":{"200":{"description":"Suggestions, strongest first.","content":{"application/json":{"schema":{"type":"object","required":["suggestions"],"properties":{"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/Suggestion"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/setup-suggestions":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"getSetupSuggestions","tags":["brands"],"summary":"The stored setup draft, if any","description":"Ten prompts and up to six competitors drafted from the brand's homepage by\n`POST`. Reading never drafts. `suggestions` is null until a draft exists.\n","responses":{"200":{"description":"The draft, or null.","content":{"application/json":{"schema":{"type":"object","required":["suggestions"],"properties":{"suggestions":{"oneOf":[{"$ref":"#/components/schemas/SetupSuggestions"},{"type":"null"}]}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}},"post":{"operationId":"createSetupSuggestions","tags":["brands"],"summary":"Draft prompts and competitors from the brand's homepage","description":"Reads the brand's homepage and drafts ten prompts and up to six competitors\nfor a person to review. Nothing is added to the brand: add the prompts that\nare kept with `POST /brands/{brandId}/prompts`, and each competitor with\n`POST /brands/{brandId}/entities`.\n\nReturns the stored draft (200, `generated: false`) when one exists, unless\n`regenerate` is true. A new draft (201) takes from a few seconds to about a\nminute. Three drafts per brand and ten per workspace per rolling day:\n`429 suggestion_limit`, with `details.retryAfterSeconds`. `503 not_configured`\nwhen drafting is unavailable; setup works without it.\n","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"regenerate":{"type":"boolean","description":"Replace the stored draft with a new one."}}}}}},"responses":{"200":{"description":"The stored draft; nothing was drafted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupSuggestionsResult"}}}},"201":{"description":"A new draft.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetupSuggestionsResult"}}}},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"`suggestion_limit`: the day's drafts are used up. `details.retryAfterSeconds` says when the next is possible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/brands/{brandId}/answers":{"parameters":[{"$ref":"#/components/parameters/brandId"}],"get":{"operationId":"listAnswers","tags":["measurement"],"summary":"The latest individual answers","description":"Newest first: the prompt, the engine, whether the brand was named and at\nwhat rank, the competitors named and the pages cited. Evidence, not a\nmetric. Do not derive a rate from it: `visibility` withholds numbers below\nten answers, and a count of these would not.\n","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}}],"responses":{"200":{"description":"Answers, newest first.","content":{"application/json":{"schema":{"type":"object","required":["answers"],"properties":{"answers":{"type":"array","items":{"$ref":"#/components/schemas/Answer"}}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"default":{"$ref":"#/components/responses/Error"}}}},"/suggestions/{suggestionId}/accept":{"parameters":[{"$ref":"#/components/parameters/suggestionId"}],"post":{"operationId":"acceptSuggestion","tags":["entities"],"summary":"Accept a suggestion as a competitor","description":"Creates the competitor entity exactly as `POST /brands/{brandId}/entities`\nwould — the same exact-alias rule, the same `context_terms_required`\nrefusal for short or common-word names — and marks the suggestion\naccepted. The name defaults to the suggested name; the domain defaults to\nthe one the answers linked, when they did.\n","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptSuggestionRequest"}}}},"responses":{"201":{"description":"Accepted; the competitor exists.","content":{"application/json":{"schema":{"type":"object","required":["suggestion","entity"],"properties":{"suggestion":{"$ref":"#/components/schemas/Suggestion"},"entity":{"$ref":"#/components/schemas/Entity"}}}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`suggestion_decided` (already accepted or dismissed) or `domain_taken`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`context_terms_required`, `invalid_alias`, `invalid_domain`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/suggestions/{suggestionId}/dismiss":{"parameters":[{"$ref":"#/components/parameters/suggestionId"}],"post":{"operationId":"dismissSuggestion","tags":["entities"],"summary":"Dismiss a suggestion","description":"Not a competitor. Quiet for ninety days, after which it may be suggested again if the answers still name it.","responses":{"200":{"description":"Dismissed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Suggestion"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`suggestion_decided`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"default":{"$ref":"#/components/responses/Error"}}}},"/openapi.json":{"get":{"operationId":"getOpenApi","tags":["discovery"],"summary":"This document","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object"}}}},"default":{"$ref":"#/components/responses/Error"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"sh_live_…","description":"An API key minted in the dashboard, or a dashboard session."}},"parameters":{"brandId":{"name":"brandId","in":"path","required":true,"schema":{"type":"string"},"description":"The brand's id, or its `slug` — both name the same brand. Responses carry the id as `brandId`."},"promptGroupId":{"name":"promptGroupId","in":"path","required":true,"schema":{"type":"string"},"description":"The identity that survives edits. Not `promptId`."},"entityId":{"name":"entityId","in":"path","required":true,"schema":{"type":"string"}},"findingId":{"name":"findingId","in":"path","required":true,"schema":{"type":"string"},"description":"The finding's id, or its `number` — the per-workspace sequence the dashboard shows and links by."},"suggestionId":{"name":"suggestionId","in":"path","required":true,"schema":{"type":"string"}},"engine":{"name":"engine","in":"query","schema":{"$ref":"#/components/schemas/Engine"},"description":"Defaults to `google_aio`."},"engineOptional":{"name":"engine","in":"query","schema":{"$ref":"#/components/schemas/Engine"},"description":"When given, coverage over the window is attached to each prompt."},"from":{"name":"from","in":"query","schema":{"type":"string","format":"date"},"description":"First day, inclusive. Defaults to 29 days before `to`. Clamped to the plan's history window."},"to":{"name":"to","in":"query","schema":{"type":"string","format":"date"},"description":"Last day, inclusive. Defaults to today (UTC)."}},"responses":{"Error":{"description":"Every failure has this body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"`not_found`: no such resource in this workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PlanLimit":{"description":"`plan_limit`: the plan does not allow this. `details` carries `limit` and `current`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["code","message","retryable"],"properties":{"code":{"type":"string","description":"Stable, machine-readable. E.g. `not_found`, `plan_limit`, `rate_limited`, `context_terms_required`."},"message":{"type":"string","description":"For a person. May change between releases."},"retryable":{"type":"boolean","description":"True when the same request may succeed later (429, 5xx)."},"details":{"type":"object","additionalProperties":true}}},"Engine":{"type":"string","enum":["google_aio","chatgpt"],"description":"`google_aio` — Google AI Overviews. `chatgpt` — ChatGPT. Google AI Mode is not tracked."},"Intent":{"type":"string","enum":["informational","comparison","commercial","navigational","problem"],"description":"Why someone would ask the prompt. `informational` — learning about a\ntopic. `comparison` — weighing named options. `commercial` — choosing\nwhat to buy. `navigational` — looking for a specific site or brand.\n`problem` — fixing something. The intent decides which diagnostic\nrules apply to the prompt's results.\n"},"DisplayableMetric":{"description":"A metric that can be shown. Never a bare number: either a point\nestimate with its 95% interval and `n`, or an explicit reason there is\nno number. `insufficient_data` is **not** zero.\n","oneOf":[{"type":"object","required":["state","point","lo","hi","n"],"properties":{"state":{"type":"string","const":"ok"},"point":{"type":"number","description":"Proportion in [0, 1]."},"lo":{"type":"number","description":"Lower bound of the 95% interval."},"hi":{"type":"number","description":"Upper bound of the 95% interval."},"n":{"type":"integer","description":"Responses the estimate rests on."}}},{"type":"object","required":["state","n","needed"],"properties":{"state":{"type":"string","const":"insufficient_data"},"n":{"type":"integer"},"needed":{"type":"integer","description":"How many more responses would cross the floor of ten."}}},{"type":"object","required":["state","n","reason"],"properties":{"state":{"type":"string","const":"undefined"},"n":{"type":"integer"},"reason":{"type":"string"}}}],"discriminator":{"propertyName":"state"}},"Entitlements":{"type":"object","description":"The limits in force for the workspace — the plan plus any one-off grant.","required":["planCode","maxBrands","maxPrompts","maxMembers","maxVisibleFindings","repsPerCycle","engines","cadence","snapshot","rechecksPerMonth","historyDays","crawlerLinesPerMonth","findingFamilies","connections","overageAllowed","runsPerPeriod","overridden"],"properties":{"planCode":{"type":"string","examples":["starter_v1"]},"maxBrands":{"type":"integer"},"maxPrompts":{"type":"integer","description":"Active prompts across every brand in the workspace."},"maxMembers":{"type":"integer","description":"People with access, the owner included. Open invites count toward it."},"maxVisibleFindings":{"type":["integer","null"],"description":"Findings shown in full, the first N in rank order. Null shows every finding. The rest are counted in a findings list's `locked`."},"repsPerCycle":{"type":"integer","description":"Repetitions per prompt per engine per cycle."},"engines":{"type":"array","items":{"$ref":"#/components/schemas/Engine"}},"cadence":{"type":"string","enum":["weekly","none"],"description":"`weekly` schedules a cycle per brand per week. `none` schedules nothing; see `snapshot`."},"snapshot":{"type":"boolean","description":"True on the free tier — one measurement at setup, once per registrable domain."},"rechecksPerMonth":{"type":"integer"},"historyDays":{"type":"integer","description":"How far back reads go. Older data is kept, not shown."},"crawlerLinesPerMonth":{"type":"integer"},"findingFamilies":{"type":"array","items":{"type":"string","enum":["ACCESS","STRUCT","CITE","FACT","COVERAGE"]}},"connections":{"type":"array","items":{"type":"string","enum":["gsc","ga4","logdrain","cloudflare_logpush"]}},"overageAllowed":{"type":"boolean"},"runsPerPeriod":{"type":"integer","description":"Hard ceiling on runs per billing period."},"overridden":{"type":"boolean","description":"True when a one-off grant changed at least one limit."}}},"Workspace":{"type":"object","required":["workspaceId","planCode","status","role","hold","entitlements","usage","brands"],"properties":{"workspaceId":{"type":"string"},"planCode":{"type":"string"},"status":{"type":"string","enum":["active","past_due","canceled"]},"role":{"type":"string","enum":["viewer","editor","admin","owner"],"description":"The caller's role in this workspace. Reads need `viewer`, writes `editor`."},"hold":{"oneOf":[{"type":"object","required":["reason","since"],"properties":{"reason":{"type":"string"},"since":{"type":["string","null"],"format":"date-time"}}},{"type":"null"}],"description":"Set when measurement is paused pending review (a cost guardrail, or an operator). Nothing runs while it is set; `POST …/cycles` answers `409 workspace_on_hold`. Null normally."},"entitlements":{"$ref":"#/components/schemas/Entitlements"},"usage":{"type":"object","required":["runsThisPeriod","runsAllowance","rechecksThisMonth","rechecksAllowance","activePrompts","promptsAllowance"],"properties":{"runsThisPeriod":{"type":"integer"},"runsAllowance":{"type":"integer"},"rechecksThisMonth":{"type":"integer"},"rechecksAllowance":{"type":"integer"},"activePrompts":{"type":"integer","description":"Active prompts across every brand."},"promptsAllowance":{"type":"integer"}}},"brands":{"type":"array","items":{"type":"object","required":["brandId","slug","name","domain"],"properties":{"brandId":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"domain":{"type":"string"}}}}}},"BrandSummary":{"type":"object","required":["brandId","slug","name","domain","timezone","createdAt","activePrompts","competitors"],"properties":{"brandId":{"type":"string"},"slug":{"type":"string","description":"The URL handle: the dashboard is at /brands/{slug}. From the name at creation; stable across renames; accepted wherever `{brandId}` is."},"name":{"type":"string"},"domain":{"type":"string","description":"Registrable domain, lowercase."},"timezone":{"type":"string","description":"IANA zone; day boundaries for cycles and rollups."},"createdAt":{"type":"string","format":"date-time"},"activePrompts":{"type":"integer"},"competitors":{"type":"integer"},"stats":{"oneOf":[{"$ref":"#/components/schemas/BrandStats"},{"type":"null"}],"description":"Present in the list; a card's worth of state."}}},"BrandStats":{"type":"object","required":["openFindings","lastCycle","nextCycle","mentionRate","trend"],"properties":{"openFindings":{"type":"integer","description":"Open and acknowledged findings, suppressed ones excluded."},"lastCycle":{"oneOf":[{"type":"object","required":["cycleId","kind","state","completedAt"],"properties":{"cycleId":{"type":"string"},"kind":{"type":"string"},"state":{"type":"string"},"completedAt":{"type":["string","null"],"format":"date-time"}}},{"type":"null"}]},"nextCycle":{"oneOf":[{"type":"object","required":["cycleId","kind","scheduledFor"],"properties":{"cycleId":{"type":"string"},"kind":{"type":"string"},"scheduledFor":{"type":"string","format":"date-time"}}},{"type":"null"}]},"mentionRate":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/DisplayableMetric"},"description":"Self mention rate over the last 30 days, keyed by entitled engine."},"trend":{"type":"object","required":["engine","points"],"properties":{"engine":{"$ref":"#/components/schemas/Engine"},"points":{"type":"array","items":{"type":"object","required":["day","rate","n"],"properties":{"day":{"type":"string","format":"date"},"rate":{"type":["number","null"],"description":"Null when the day's n is below ten. A gap, not a zero."},"n":{"type":"integer"}}}}},"description":"Daily self mention rate for the primary engine over the last 56 days, rollup days only."}}},"CycleHealth":{"type":"object","required":["cycleId","state","kind","at","plannedRuns","completedRuns","failedRuns"],"properties":{"cycleId":{"type":"string"},"state":{"type":"string"},"kind":{"type":"string","enum":["scheduled","recheck","manual"]},"at":{"type":"string","format":"date-time","description":"Completion time for a finished cycle, else its due time."},"plannedRuns":{"type":"integer"},"completedRuns":{"type":"integer"},"failedRuns":{"type":"integer"}}},"ConnectionHealth":{"type":"object","required":["kind","status","externalRef","lastSyncAt","lastError","hoursSinceSync"],"properties":{"kind":{"type":"string"},"status":{"type":"string","enum":["active","needs_reauth","error","revoked"]},"externalRef":{"type":["string","null"]},"lastSyncAt":{"type":["string","null"],"format":"date-time"},"lastError":{"type":["string","null"]},"hoursSinceSync":{"type":["number","null"]}}},"BrandHealth":{"type":"object","required":["lastSuccessfulCycle","nextScheduledCycle","runningCycle","runSuccessRate","ingest","connections"],"properties":{"lastSuccessfulCycle":{"oneOf":[{"$ref":"#/components/schemas/CycleHealth"},{"type":"null"}]},"nextScheduledCycle":{"oneOf":[{"$ref":"#/components/schemas/CycleHealth"},{"type":"null"}],"description":"Distinguishes \"nothing running\" from \"nothing scheduled\"."},"runningCycle":{"oneOf":[{"$ref":"#/components/schemas/CycleHealth"},{"type":"null"}]},"runSuccessRate":{"type":"object","required":["succeeded","failed","total","rate","windowDays"],"properties":{"succeeded":{"type":"integer"},"failed":{"type":"integer"},"total":{"type":"integer"},"rate":{"type":["number","null"],"description":"Null when nothing ran — not 0."},"windowDays":{"type":"integer"}}},"ingest":{"type":"object","required":["everIngested","lastDay","daysSince","daysWithDataLast30","linesThisMonth"],"properties":{"everIngested":{"type":"boolean"},"lastDay":{"type":["string","null"],"format":"date"},"daysSince":{"type":["integer","null"]},"daysWithDataLast30":{"type":"integer"},"linesThisMonth":{"type":"integer","description":"Log lines delivered this calendar month across the workspace. Compare with `entitlements.crawlerLinesPerMonth`: once used up, further lines are dropped at ingest until next month and ingest days stop being recorded."}}},"connections":{"type":"array","items":{"$ref":"#/components/schemas/ConnectionHealth"}}}},"BrandDetail":{"allOf":[{"$ref":"#/components/schemas/BrandSummary"},{"type":"object","required":["engines","health"],"properties":{"engines":{"type":"array","items":{"$ref":"#/components/schemas/Engine"},"description":"The engines the plan measures for this brand."},"health":{"$ref":"#/components/schemas/BrandHealth"}}}]},"CreateBrandRequest":{"type":"object","required":["name","domain"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120,"examples":["Waitlister"]},"domain":{"type":"string","description":"A host or URL; reduced to its registrable domain (`www.waitlister.me/pricing` → `waitlister.me`).","examples":["waitlister.me"]},"timezone":{"type":"string","description":"IANA zone. Defaults to UTC.","examples":["Europe/Helsinki"]},"contextTerms":{"type":"array","items":{"type":"string","maxLength":80},"description":"Words that appear near the name when it means the company — a product category, the domain. Required when the name is six characters or fewer or an ordinary English word; a mention then counts only near one of them.","examples":[["shared inbox","front.com"]]}}},"UpdateBrandRequest":{"type":"object","description":"At least one field. `400 empty_patch` otherwise.","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"timezone":{"type":"string","description":"IANA zone.","examples":["Europe/Helsinki"]}}},"BrandCreated":{"type":"object","required":["brandId","slug","name","domain","timezone","selfEntityId","engines","firstCycle","snapshot"],"properties":{"brandId":{"type":"string"},"slug":{"type":"string","description":"The URL handle, derived from the name. See BrandSummary."},"name":{"type":"string"},"domain":{"type":"string"},"timezone":{"type":"string"},"selfEntityId":{"type":"string","description":"The brand's own entity; edit its aliases with `PUT /entities/{entityId}`."},"engines":{"type":"array","items":{"$ref":"#/components/schemas/Engine"}},"firstCycle":{"oneOf":[{"type":"object","required":["cycleId","kind","scheduledFor"],"properties":{"cycleId":{"type":"string"},"kind":{"type":"string","enum":["scheduled","manual"]},"scheduledFor":{"type":"string","format":"date-time"}}},{"type":"null"}],"description":"The cycle planted for the next sweep. Null when none was (free tier, domain already measured)."},"snapshot":{"type":["string","null"],"enum":["planned","already_taken",null],"description":"Free tier only. `already_taken` means this registrable domain has had its free measurement, in this workspace or another; nothing will run."}}},"Prompt":{"type":"object","required":["promptGroupId","promptId","text","intent","tags","active","createdAt","version"],"properties":{"promptGroupId":{"type":"string","description":"Stable across edits. Use it for trends and for `PATCH`/`DELETE`."},"promptId":{"type":"string","description":"The current version's id. Changes when text or intent is edited."},"text":{"type":"string"},"intent":{"$ref":"#/components/schemas/Intent"},"tags":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"version":{"type":"integer","minimum":1}}},"PromptCoverage":{"type":"object","required":["promptGroupId","text","intent","nResponses","selfMentionCount","unbrandedCount","emptyCount","selfMentionRate"],"properties":{"promptGroupId":{"type":"string"},"text":{"type":"string"},"intent":{"type":"string"},"nResponses":{"type":"integer"},"selfMentionCount":{"type":"integer"},"unbrandedCount":{"type":"integer","description":"Responses that mentioned no tracked entity at all."},"emptyCount":{"type":"integer","description":"Responses where the engine produced no AI answer."},"selfMentionRate":{"$ref":"#/components/schemas/DisplayableMetric"}}},"PromptList":{"type":"object","required":["prompts","window"],"properties":{"prompts":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Prompt"},{"type":"object","properties":{"coverage":{"oneOf":[{"$ref":"#/components/schemas/PromptCoverage"},{"type":"null"}],"description":"Present only when `engine` was given."}}}]}},"window":{"oneOf":[{"type":"object","required":["engine","from","to","historyFrom"],"properties":{"engine":{"$ref":"#/components/schemas/Engine"},"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"historyFrom":{"type":["string","null"],"format":"date"}}},{"type":"null"}]}}},"NewPrompt":{"type":"object","required":["text","intent"],"properties":{"text":{"type":"string","minLength":1,"maxLength":500,"examples":["best waitlist software for a product launch"]},"intent":{"$ref":"#/components/schemas/Intent"},"tags":{"type":"array","items":{"type":"string","maxLength":40}}}},"AddPromptsRequest":{"type":"object","required":["prompts"],"properties":{"prompts":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/NewPrompt"}}}},"PromptPatch":{"type":"object","minProperties":1,"properties":{"text":{"type":"string","minLength":1,"maxLength":500},"intent":{"$ref":"#/components/schemas/Intent"},"tags":{"type":"array","items":{"type":"string","maxLength":40}},"active":{"type":"boolean"}}},"Alias":{"type":"object","required":["aliasId","alias","kind","caseSensitive"],"properties":{"aliasId":{"type":"string"},"alias":{"type":"string"},"kind":{"type":"string","enum":["name","product","abbreviation","misspelling"]},"caseSensitive":{"type":"boolean","description":"True for acronyms: `SAP`, not `sap`."}}},"AliasInput":{"type":"object","required":["alias"],"properties":{"alias":{"type":"string","minLength":1,"maxLength":80,"description":"An exact string, matched on token boundaries. Not a pattern."},"kind":{"type":"string","enum":["name","product","abbreviation","misspelling"],"default":"name"},"caseSensitive":{"type":"boolean","default":false}}},"EntityConfig":{"type":"object","properties":{"aliases":{"type":"array","items":{"$ref":"#/components/schemas/AliasInput"},"description":"Defaults to the entity's name."},"domains":{"type":"array","items":{"type":"string"},"description":"Hosts or URLs; reduced to registrable domains. A citation of one of them counts for this entity."},"exclusions":{"type":"array","items":{"type":"string","maxLength":80},"description":"Literal phrases that contain an alias but are not the entity (\"arc welding\" for \"Arc\")."},"contextTerms":{"type":"array","items":{"type":"string","maxLength":80},"description":"When non-empty, a mention counts only if one of these appears nearby. Required for short or common-word names."}}},"CreateEntityRequest":{"allOf":[{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120,"examples":["LaunchList"]}}},{"$ref":"#/components/schemas/EntityConfig"}]},"Entity":{"type":"object","required":["entityId","name","isSelf","requiresContext","needsContextTerms","contextTerms","aliases","domains","exclusions","createdAt"],"properties":{"entityId":{"type":"string"},"name":{"type":"string"},"isSelf":{"type":"boolean","description":"True for the brand itself."},"requiresContext":{"type":"boolean","description":"Mentions count only near one of `contextTerms`."},"needsContextTerms":{"type":"boolean","description":"The name is six characters or fewer or an ordinary English word, so it needs context terms to count only real mentions. True with `requiresContext` false means the entity is matching without them; set some with PUT /entities/{entityId}."},"contextTerms":{"type":"array","items":{"type":"string"}},"aliases":{"type":"array","items":{"$ref":"#/components/schemas/Alias"}},"domains":{"type":"array","items":{"type":"string"}},"exclusions":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"}}},"Cycle":{"type":"object","required":["cycleId","kind","state","scheduledFor","completedAt","plannedRuns","completedRuns","failedRuns"],"properties":{"cycleId":{"type":"string"},"kind":{"type":"string","enum":["scheduled","recheck","manual"]},"state":{"type":"string","enum":["pending","running","complete","partial","failed","skipped_quota"],"description":"`partial` — some runs failed. `skipped_quota` — the allowance, the plan or billing stopped it."},"scheduledFor":{"type":"string","format":"date-time"},"completedAt":{"type":["string","null"],"format":"date-time"},"plannedRuns":{"type":"integer"},"completedRuns":{"type":"integer"},"failedRuns":{"type":"integer"}}},"CycleCreated":{"type":"object","required":["cycleId","kind","state","scheduledFor"],"properties":{"cycleId":{"type":"string"},"kind":{"type":"string","const":"manual"},"state":{"type":"string","const":"pending"},"scheduledFor":{"type":"string","format":"date-time"}}},"EntityVisibility":{"type":"object","required":["entityId","canonicalName","isSelf","mentionRate","shareOfVoice","sovInterval","avgProminence","modelIds"],"properties":{"entityId":{"type":"string"},"canonicalName":{"type":"string"},"isSelf":{"type":"boolean"},"mentionRate":{"allOf":[{"$ref":"#/components/schemas/DisplayableMetric"}],"description":"Share of responses that mentioned the entity. Its `n` is the responses measured while the entity was tracked, fewer than `nResponses` for a competitor added inside the range."},"shareOfVoice":{"allOf":[{"$ref":"#/components/schemas/DisplayableMetric"}],"description":"The entity's share of mentions among the entities tracked at the end of the range. Its `n` is the responses checked against all of them, so it restarts when a competitor is added."},"sovInterval":{"type":"string","enum":["bootstrap","pooled_daily","none"],"description":"How the share-of-voice interval was obtained. `pooled_daily` is approximate (slightly narrow)."},"avgProminence":{"type":["number","null"],"description":"Mean position of first mention, 0 = start of answer, 1 = end. Lower is more prominent."},"modelIds":{"type":"array","items":{"type":"string"},"description":"Provider model identifiers seen in the window. A change here can move metrics on its own."}}},"Visibility":{"type":"object","required":["brand","engine","range","historyFrom","asOf","nResponses","entities","prompts"],"properties":{"brand":{"type":"object","required":["brandId","name","timezone"],"properties":{"brandId":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string"}}},"engine":{"$ref":"#/components/schemas/Engine"},"range":{"type":"object","required":["from","to"],"properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}}},"historyFrom":{"type":["string","null"],"format":"date","description":"Set when `from` was moved forward to the plan's history window."},"asOf":{"type":["string","null"],"format":"date","description":"The day the numbers describe, when a closed cycle served them."},"nResponses":{"type":"integer"},"entities":{"type":"array","items":{"$ref":"#/components/schemas/EntityVisibility"}},"prompts":{"type":"array","items":{"$ref":"#/components/schemas/PromptCoverage"}}}},"SeriesPoint":{"type":"object","required":["asOf","n","mentionRate","shareOfVoice","avgProminence","modelIds"],"properties":{"asOf":{"type":"string","format":"date","description":"The cycle-close day this window ends on."},"n":{"type":"integer","description":"Responses in the window measured while the entity was tracked: the mention rate's n."},"mentionRate":{"allOf":[{"$ref":"#/components/schemas/DisplayableMetric"}],"description":"Share of the window's responses that mentioned the entity, with its Wilson interval."},"shareOfVoice":{"allOf":[{"$ref":"#/components/schemas/DisplayableMetric"}],"description":"Among the entities tracked at the window's end, over the responses checked against all of them; its own n."},"avgProminence":{"type":["number","null"]},"modelIds":{"type":"array","items":{"type":"string"}}}},"VisibilitySeries":{"type":"object","required":["brand","engine","windowDays","range","historyFrom","points","entities","modelChanges"],"properties":{"brand":{"type":"object","required":["brandId","name","timezone"],"properties":{"brandId":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string"}}},"engine":{"$ref":"#/components/schemas/Engine"},"windowDays":{"type":"integer","enum":[7,30,90]},"range":{"type":"object","required":["from","to"],"properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}}},"historyFrom":{"type":["string","null"],"format":"date","description":"Set when `from` was moved forward to the plan's history window."},"points":{"type":"array","items":{"type":"string","format":"date"},"description":"Cycle-close days in the range, ascending. Every entity's `series` is index-aligned with this."},"entities":{"type":"array","description":"The brand first, then competitors by how often they were named over the range.","items":{"type":"object","required":["entityId","canonicalName","isSelf","series"],"properties":{"entityId":{"type":"string"},"canonicalName":{"type":"string"},"isSelf":{"type":"boolean"},"series":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/SeriesPoint"},{"type":"null"}]},"description":"One entry per `points` day; null when the rollup has no row for the entity on that close."}}}},"modelChanges":{"type":"array","description":"Closes where the provider's model set differed from the previous close. A comparison across one is confounded.","items":{"type":"object","required":["asOf","from","to"],"properties":{"asOf":{"type":"string","format":"date"},"from":{"type":"array","items":{"type":"string"}},"to":{"type":"array","items":{"type":"string"}}}}}}},"BotActivity":{"type":"object","required":["botId","purpose","vendor","verifiedHits","unverifiedHits","distinctPaths","status4xx","status5xx","bytes","verifiable"],"properties":{"botId":{"type":"string","examples":["GPTBot"]},"purpose":{"type":"string","enum":["training","live_retrieval"],"description":"`live_retrieval` fetches pages to answer a question now, so a page it cannot fetch cannot be cited in those answers; `training` collects pages for model training."},"vendor":{"type":"string"},"verifiedHits":{"type":"integer","description":"Hits whose IP was confirmed against the vendor's published ranges. The only number that is a signal."},"unverifiedHits":{"type":"integer","description":"Hits that merely claimed the user-agent. Anyone can send one. Never add to verifiedHits."},"distinctPaths":{"type":"integer"},"status4xx":{"type":"integer"},"status5xx":{"type":"integer"},"bytes":{"type":"integer"},"verifiable":{"type":"boolean","description":"False for bots whose vendor publishes no ranges; their hits can never be verified."}}},"IngestCoverage":{"type":"object","required":["daysWithIngest","windowDays","continuous","lastSeen","gaps"],"properties":{"daysWithIngest":{"type":"integer"},"windowDays":{"type":"integer"},"continuous":{"type":"boolean","description":"Unbroken daily ingest across the window."},"lastSeen":{"type":["string","null"],"format":"date"},"gaps":{"type":"array","items":{"type":"string","format":"date"},"description":"Days in the window with no log lines received."}}},"Crawlers":{"type":"object","required":["brand","range","historyFrom","coverage","bots","daily","totals"],"properties":{"brand":{"type":"object","required":["brandId","name"],"properties":{"brandId":{"type":"string"},"name":{"type":"string"}}},"range":{"type":"object","required":["from","to"],"properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}}},"historyFrom":{"type":["string","null"],"format":"date"},"coverage":{"$ref":"#/components/schemas/IngestCoverage"},"bots":{"type":"array","items":{"$ref":"#/components/schemas/BotActivity"}},"daily":{"type":"object","required":["days","ingested","bots"],"description":"Hits per day for the timeline. A day with no log ingest is null in\nevery series — a gap, not a zero. Verified and unverified stay apart\nhere too.\n","properties":{"days":{"type":"array","items":{"type":"string","format":"date"},"description":"Every day in the range, ascending. The per-bot series are index-aligned with it."},"ingested":{"type":"array","items":{"type":"boolean"}},"bots":{"type":"array","description":"Most verified hits first.","items":{"type":"object","required":["botId","purpose","vendor","verified","unverified"],"properties":{"botId":{"type":"string"},"purpose":{"type":"string","enum":["training","live_retrieval"]},"vendor":{"type":"string"},"verified":{"type":"array","items":{"type":["integer","null"]}},"unverified":{"type":"array","items":{"type":["integer","null"]}}}}}}},"totals":{"type":"object","required":["verified","unverified","liveRetrieval","training"],"properties":{"verified":{"type":"integer"},"unverified":{"type":"integer"},"liveRetrieval":{"type":"integer","description":"Verified hits by live-retrieval bots."},"training":{"type":"integer","description":"Verified hits by training crawlers."}}}}},"Recommendation":{"type":"object","required":["summary","steps","target","expectedMetric"],"properties":{"summary":{"type":"string"},"steps":{"type":"array","items":{"type":"string"}},"target":{"type":"string","description":"The specific artifact to change — a URL, a file, a setting."},"expectedMetric":{"type":"string","description":"Which metric should move if the fix works."}}},"Finding":{"type":"object","required":["findingId","number","ruleId","family","severity","confidence","state","subjectType","subjectRef","evidence","recommendation","firstSeenAt","lastSeenAt","suppressedBy","surfaced","estimatedImpact"],"properties":{"findingId":{"type":"string"},"number":{"type":"integer","description":"1, 2, 3… per workspace, never reused. The dashboard links by it (/brands/{slug}/findings/{number}) and `{findingId}` accepts it."},"ruleId":{"type":"string","examples":["ACCESS_ROBOTS_BLOCKS_AI"]},"family":{"type":"string","enum":["ACCESS","STRUCT","CITE","FACT","COVERAGE"]},"severity":{"type":"string","enum":["critical","high","medium","low"]},"confidence":{"type":"string","enum":["observed","inferred","heuristic"],"description":"`observed` — measured from first-party data. `inferred` — a reliable pattern. `heuristic` — plausible, unconfirmed; treat as a hypothesis."},"state":{"type":"string","enum":["open","acknowledged","fix_applied","rechecking","resolved","not_moved","dismissed","stale"]},"subjectType":{"type":"string","enum":["url","prompt_group","domain","brand"]},"subjectRef":{"type":"string"},"evidence":{"type":"object","additionalProperties":true,"description":"What the rule saw. Shape depends on `ruleId`."},"recommendation":{"$ref":"#/components/schemas/Recommendation"},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"suppressedBy":{"type":["string","null"],"description":"The finding that explains this one, when suppressed."},"surfaced":{"type":"boolean","description":"False while the finding waits behind the weekly cap."},"estimatedImpact":{"type":"number"}}},"FindingEvent":{"type":"object","required":["eventId","from","to","actor","actorName","note","payload","at"],"properties":{"eventId":{"type":"string"},"from":{"type":["string","null"]},"to":{"type":"string"},"actor":{"type":"string","description":"A user id, or `system`."},"actorName":{"type":["string","null"],"description":"The member's name, else their email, when a person acted. Null for `system`."},"note":{"type":["string","null"]},"payload":{"type":["object","null"],"additionalProperties":true,"description":"What the transition recorded. A recheck verdict (`to` = `resolved` or\n`not_moved`) carries `results`: one entry per prompt group compared, with\n`verdict`, `delta`, `baselineInterval`, `afterInterval`, `p`, both gates,\n`confounded` and `modelIdDrift` (metrics.md §6). It also carries `pooled`:\nthe same fields for the groups taken together, plus `groups`, or null when\nfewer than two were compared. `resolved` means the pooled comparison or at\nleast one group moved. A structural confirmation carries `structural`: what\nthe check saw.\n"},"at":{"type":"string","format":"date-time"}}},"FindingList":{"type":"object","required":["brand","findings","counts","queued","locked"],"properties":{"brand":{"type":"object","required":["brandId","name","domain"],"properties":{"brandId":{"type":"string"},"name":{"type":"string"},"domain":{"type":"string"}}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/Finding"}},"counts":{"type":"object","additionalProperties":{"type":"integer"},"description":"Findings per state, across all states."},"queued":{"type":"integer","description":"Open findings not yet surfaced because of the weekly cap."},"locked":{"type":"object","required":["count","bySeverity","limit"],"description":"Findings the plan does not show in full (`maxVisibleFindings`): counted, never sent.","properties":{"count":{"type":"integer"},"bySeverity":{"type":"object","additionalProperties":{"type":"integer"}},"limit":{"type":["integer","null"]}}}}},"FindingDetail":{"allOf":[{"$ref":"#/components/schemas/Finding"},{"type":"object","required":["brandId","suppressedByNumber","stateChangedAt","baseline","events"],"properties":{"brandId":{"type":"string"},"suppressedByNumber":{"type":["integer","null"],"description":"The `number` of `suppressedBy`, when suppressed."},"stateChangedAt":{"type":"string","format":"date-time"},"baseline":{"type":["object","null"],"additionalProperties":true,"description":"The snapshot captured at `fix_applied`, which a recheck verdict is\nmeasured against: `{ capturedAt, window: { from, to }, promptGroups:\n[ { promptGroupId, x, n, modelIds } ] }`. Null before a fix is marked applied.\n"},"events":{"type":"array","items":{"$ref":"#/components/schemas/FindingEvent"},"description":"Newest first."}}}]},"TransitionRequest":{"type":"object","required":["to"],"properties":{"to":{"type":"string","enum":["acknowledged","fix_applied","dismissed"],"description":"`fix_applied` starts the recheck clock. `resolved`/`not_moved` are decided by the recheck."},"note":{"type":"string","maxLength":2000}}},"TransitionResult":{"type":"object","required":["findingId","number","from","to","baselineCaptured","recheck"],"properties":{"findingId":{"type":"string"},"number":{"type":"integer"},"from":{"type":"string"},"to":{"type":"string"},"baselineCaptured":{"type":"boolean"},"recheck":{"oneOf":[{"type":"object","required":["promptGroups","inDays"],"properties":{"promptGroups":{"type":"integer","description":"Prompts whose metrics the recheck will compare."},"inDays":{"type":"integer","description":"Days until the recheck cycle runs."}}},{"type":"null"}]}}},"IngestToken":{"type":"object","required":["token","tokenPrefix","label","endpoint","revokedPrevious","setup"],"properties":{"token":{"type":"string","description":"Shown once. Stored hashed; a lost token is replaced, not recovered."},"tokenPrefix":{"type":"string","description":"Enough to recognise the token in settings, never enough to use it."},"label":{"type":"string"},"endpoint":{"type":"string","format":"uri","description":"Where to POST log lines, with `Authorization: Bearer <token>`."},"revokedPrevious":{"type":"integer","description":"How many earlier tokens this one replaced."},"setup":{"type":"object","description":"The three ways to get logs to the endpoint. Show the one that fits the site.","required":["httpDrain","cloudflareLogpush","cloudflareWorker"],"properties":{"httpDrain":{"type":"object","required":["endpoint","header","contentType","note"],"properties":{"endpoint":{"type":"string","format":"uri"},"header":{"type":"string","const":"X-Shruwd-Ingest-Token"},"contentType":{"type":"string","const":"application/x-ndjson"},"note":{"type":"string"}}},"cloudflareLogpush":{"type":"object","description":"A Logpush job with an HTTP destination (Cloudflare Enterprise zones).","required":["destinationUrl","dataset","fields","filter","note"],"properties":{"destinationUrl":{"type":"string","format":"uri","description":"The endpoint with the token as a `header_Authorization` URL parameter, which Logpush turns into a request header. Contains the token."},"dataset":{"type":"string","const":"http_requests"},"fields":{"type":"array","items":{"type":"string"}},"filter":{"type":"string","description":"The job's filter as the Logpush API takes it, a JSON string, keeping the job to the brand's domain and its www host."},"note":{"type":"string"}}},"cloudflareWorker":{"type":"object","description":"The reference forwarder Worker for any other Cloudflare plan or CDN-served site.","required":["note","ingestUrl","secretName"],"properties":{"note":{"type":"string"},"ingestUrl":{"type":"string","format":"uri"},"secretName":{"type":"string","const":"SHRUWD_INGEST_TOKEN"}}}}}}},"SetupSuggestions":{"type":"object","description":"A draft for a person to review. Nothing in it is tracked or measured until it is added.","required":["brandId","sourceUrl","prompts","competitors","generatedAt"],"properties":{"brandId":{"type":"string"},"sourceUrl":{"type":["string","null"],"description":"The page it was read from. Null when the site could not be fetched and the draft rests on the name and domain."},"prompts":{"type":"array","items":{"type":"object","required":["text","intent"],"properties":{"text":{"type":"string"},"intent":{"$ref":"#/components/schemas/Intent"}}}},"competitors":{"type":"array","items":{"type":"object","required":["name","domain"],"properties":{"name":{"type":"string"},"domain":{"type":["string","null"],"description":"Registrable domain, when the draft was confident of one."}}}},"generatedAt":{"type":"string","format":"date-time"}}},"SetupSuggestionsResult":{"type":"object","required":["suggestions","generated"],"properties":{"suggestions":{"$ref":"#/components/schemas/SetupSuggestions"},"generated":{"type":"boolean","description":"False when the stored draft was returned."}}},"Answer":{"type":"object","description":"One answer as the engine gave it. Evidence, not a metric.","required":["runId","promptGroupId","prompt","engine","state","executedAt","mentioned","rank","context","competitors","citations"],"properties":{"runId":{"type":"string"},"promptGroupId":{"type":"string"},"prompt":{"type":"string"},"engine":{"$ref":"#/components/schemas/Engine"},"state":{"type":"string","enum":["succeeded","empty"],"description":"`empty`: the engine gave no AI answer for the prompt. An observation, not a failure."},"executedAt":{"type":"string","format":"date-time"},"mentioned":{"type":"boolean"},"rank":{"type":["integer","null"],"description":"Order of first appearance among the tracked names, 1-based, when mentioned."},"context":{"type":["string","null"],"description":"About 200 characters either side of the brand's first mention."},"competitors":{"type":"array","items":{"type":"object","required":["entityId","name","rank"],"properties":{"entityId":{"type":"string"},"name":{"type":"string"},"rank":{"type":"integer"}}}},"citations":{"type":"array","items":{"type":"object","required":["url","domain","title","isSelf"],"properties":{"url":{"type":"string"},"domain":{"type":"string"},"title":{"type":["string","null"]},"isSelf":{"type":"boolean"}}}}}},"Suggestion":{"type":"object","description":"A name the answers use that the brand does not track, with the evidence for suggesting it.","required":["suggestionId","brandId","name","domainHint","state","share","responses","clusterResponses","cluster","samplePromptGroupId","firstSeenAt","lastSeenAt","decidedAt","entityId"],"properties":{"suggestionId":{"type":"string"},"brandId":{"type":"string"},"name":{"type":"string","description":"The most frequent surface form in the answers."},"domainHint":{"type":["string","null"],"description":"The registrable domain the answers linked the name to, when they did. Used as the entity's domain on acceptance unless overridden."},"state":{"type":"string","enum":["open","accepted","dismissed"]},"share":{"type":"number","description":"Distinct responses in the cluster that named it, divided by the cluster's responses. In [0, 1]."},"responses":{"type":"integer"},"clusterResponses":{"type":"integer"},"cluster":{"type":"string","description":"`*` for the whole prompt set, otherwise a prompt tag."},"samplePromptGroupId":{"type":["string","null"],"description":"One prompt whose answers named it."},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"decidedAt":{"type":["string","null"],"format":"date-time"},"entityId":{"type":["string","null"],"description":"The competitor it became, once accepted."}}},"AcceptSuggestionRequest":{"allOf":[{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120,"description":"Defaults to the suggested name."}}},{"$ref":"#/components/schemas/EntityConfig"}]}}}}