{"openapi":"3.0.3","info":{"title":"SignalKit API","version":"1.0.0","description":"API for tracking brand visibility across LLM platforms (ChatGPT, Claude, Gemini, Perplexity, and more). All data routes are scoped to a project. Specify the active project via the X-Project-Id header or ?projectId query parameter. If omitted, the user's default project is used."},"servers":[{"url":"https://signalkit.ai","description":"Production"}],"components":{"securitySchemes":{"cookieAuth":{"type":"apiKey","in":"cookie","name":"__session","description":"Clerk session cookie, set automatically after login. Browser clients send it implicitly; server-to-server callers should use an API key instead."},"apiKeyAuth":{"type":"http","scheme":"bearer","description":"API key created via the /api/api-keys endpoint. Use as: Authorization: Bearer sk_live_..."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}}},"TrafficDelta":{"type":"object","required":["now","before","delta","unit","verdict"],"properties":{"now":{"type":"number","nullable":true},"before":{"type":"number","nullable":true},"delta":{"type":"number","nullable":true},"unit":{"type":"string","enum":["pts","pct","count","rank"]},"verdict":{"type":"string","enum":["up","down","flat","uncertain","refused","first_period"]},"reason":{"type":"string"},"intervalVerdict":{"type":"string","enum":["intervals_separated","uncertain"]},"interval":{"type":"array","nullable":true,"minItems":2,"maxItems":2,"items":{"type":"number"}}}},"LlmTrafficResponse":{"type":"object","required":["totalSessions","llmSessions","llmShare","llmRevenue","integrationStatus","period","previousPeriod","granularity","series","deltas","conversions"],"properties":{"currencyCode":{"type":"string","nullable":true,"description":"The GA4 property's ISO 4217 currency when known. Every money figure is in it; none is converted or assumed to be USD."},"totalSessions":{"type":"integer"},"llmSessions":{"type":"integer"},"llmShare":{"type":"number","nullable":true,"description":"0–1; null when the property recorded no session in the window."},"llmRevenue":{"type":"number","description":"GA4 total revenue (purchases, subscriptions and ad revenue, net of refunds) in AI-referred sessions, in currencyCode."},"llmRevenueShare":{"type":"number","nullable":true,"description":"0–1; null when the property recorded no revenue in the window."},"bySource":{"type":"array","description":"Per-assistant traffic and engagement totals.","items":{"type":"object"}},"topLandingPages":{"type":"array","description":"Landing-page rows whose conversions and revenue are AI-attributed.","items":{"type":"object"}},"integrationStatus":{"type":"string"},"integrationLastError":{"type":"string"},"period":{"type":"object","required":["from","to","days"],"description":"The window actually read. It ends at the newest day GA4 has finished counting, three days before today, so a range ending later is read to that day.","properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"days":{"type":"integer"}}},"previousPeriod":{"type":"object","nullable":true,"required":["from","to"],"properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}}},"granularity":{"type":"string","enum":["day","week","month"]},"series":{"type":"object","required":["llmSessions","byPlatform"],"properties":{"llmSessions":{"type":"array","description":"Dense session points at the requested granularity.","items":{"type":"object","required":["x","y"],"properties":{"x":{"type":"string","format":"date"},"y":{"type":"integer"}}}},"byPlatform":{"type":"array","items":{"type":"object","required":["key","label","points"],"properties":{"key":{"type":"string"},"label":{"type":"string"},"points":{"type":"array","items":{"type":"object"}}}}}}},"deltas":{"type":"object","required":["llmSessions","llmRevenue","llmShare","conversionRate"],"properties":{"llmSessions":{"$ref":"#/components/schemas/TrafficDelta"},"llmRevenue":{"$ref":"#/components/schemas/TrafficDelta"},"llmShare":{"$ref":"#/components/schemas/TrafficDelta"},"conversionRate":{"$ref":"#/components/schemas/TrafficDelta"}}},"conversions":{"type":"object","required":["llm","eventSelected","eventName","eventRate","eventUnavailable"],"properties":{"llm":{"type":"number","description":"Every GA4 key event in AI-referred sessions: events, not sessions."},"eventSelected":{"type":"boolean"},"eventName":{"type":"string","nullable":true},"eventRate":{"type":"number","nullable":true},"eventUnavailable":{"type":"boolean"}}}}},"GaConversionEventSelection":{"type":"object","required":["events","conversionEvent","registered"],"properties":{"events":{"type":"array","items":{"type":"string"}},"conversionEvent":{"type":"string","nullable":true},"registered":{"type":"boolean","description":"False when the saved event is absent from the property's current registered events."}}},"RateResult":{"type":"object","description":"A rate in percentage units with the counts it is a ratio of. rate and interval are null for an empty cohort.","properties":{"rate":{"type":"number","nullable":true},"numerator":{"type":"integer"},"denominator":{"type":"integer"},"interval":{"type":"array","nullable":true,"items":{"type":"number"},"minItems":2,"maxItems":2}}},"Brand":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"domain":{"type":"string","nullable":true},"aliases":{"type":"array","nullable":true,"items":{"type":"string"}},"isOwnBrand":{"type":"boolean"},"isCompetitor":{"type":"boolean"},"totalMentions":{"type":"integer"},"avgPosition":{"type":"number","nullable":true},"avgSentimentScore":{"type":"number","nullable":true},"mentionsByLlm":{"type":"object","additionalProperties":{"type":"integer"}},"eligibleAnswers":{"type":"integer","nullable":true},"mentionedAnswers":{"type":"integer","nullable":true},"metricVersion":{"type":"integer","nullable":true},"mentionRate":{"allOf":[{"$ref":"#/components/schemas/RateResult"}],"nullable":true,"description":"Lifetime: mentionedAnswers over eligibleAnswers. Null until the first recompute."}}},"MeasuredCohort":{"type":"object","description":"The measured block the overview and trends share: one cohort, counts beside every rate, and a reason wherever a figure is withheld.","properties":{"range":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"days":{"type":"integer"}}},"cohort":{"type":"object","description":"What was asked for and what was measured: projectId, range, platforms, regions, queryClass, measured, promptClasses, classifiable, mentionCountMode, and models. Each models row keeps exact raw model IDs, provider routes and observed search counts; displayLabel and identityNote make an upstream model vendor explicit when it differs from the named platform."},"health":{"type":"object","description":"lastMeasuredAt, partialCurrentDay, measuredDays, calculationVersionLabel, calculationCoverage (selected dominant canonical contract, included/excluded answer counts and excluded versions), notes. Related headline, ranking, series and comparison reads use the same selected contract. Native citation support has a separate denominator from conclusive naming evidence."},"metrics":{"type":"object","properties":{"mentionRate":{"type":"object","description":"definition, pooled (RateResult or null), versionSlices, suppressed, slicing."},"citedRate":{"type":"object","description":"definition, pooled."},"meanPosition":{"type":"object","description":"definition, mean, rankedAnswers."},"sentiment":{"type":"object","description":"definition, score, positive, neutral, negative."},"platformCoverage":{"type":"object","description":"definition, named, measured, platforms."},"eligibleAnswers":{"type":"object","description":"definition, count."}}},"series":{"type":"array","items":{"type":"object","description":"date, rate, numerator, denominator, version."}},"platformSlices":{"type":"array","items":{"type":"object","description":"platform, mentionRate, citedRate, points."}},"comparison":{"type":"object","description":"comparable, verdict, reason, baselineRange, baseline, current, mentionRateChange (value with percentage_points unit), changePoints (null; change-point detection is not implemented), prompts, platforms, regions, cadenceShifts, platformSlices, disclosures, and basis + likeForLike when the comparison was made on the prompt × platform × region cells both periods measured (ADR 0054): basis is \"like_for_like\" and likeForLike counts what was kept of prompts, platforms (with the platforms excluded for collapsing), regions and answers. reason is \"insufficient_overlap\" when those cells hold under 70% of the current answers.","properties":{"basis":{"type":"string","enum":["like_for_like"]},"likeForLike":{"type":"object","description":"prompts, platforms and regions as { common, current, previous, all } (platforms also carries excluded: [{ platform, now, before }]), answers as { common, current, commonPrevious, previous }, and answersShare (common current answers over current answers outside a platform that switched model, 0-1)."}}},"rankings":{"type":"array","items":{"type":"object","description":"id, name, domain, aliases, isOwnBrand, isCompetitor, rank, mentionRate, answersNaming, totalMentions, promptsNaming, sovPct, avgPosition, sentimentScore."}},"shareOfVoice":{"type":"object","description":"definition, mode, totalOccurrences, trackedBrands, entries, series."}}},"Segment":{"type":"object","required":["id","projectId","name","slug"],"description":"A named subset of a project's prompts plus the brands it counts as competitors. Every project has exactly one default segment, named after the project, and it cannot be deleted.","properties":{"id":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string","description":"What `?segment=` carries in a dashboard URL. Unique within the project."},"kind":{"type":"string","enum":["standard","catalog"]},"rosterMode":{"type":"string","enum":["project","own"],"description":"`project` compares against every brand the project tracks. `own` compares against this segment's own roster, plus the own brand, which is always in it."},"isDefault":{"type":"boolean"},"position":{"type":"integer"},"promptCount":{"type":"integer","description":"Prompts in this segment, archived included: it is what a delete refuses on."},"brandIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Roster membership. Empty in `project` mode, where the project's roster applies."},"createdAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true}}},"Prompt":{"type":"object","required":["id","text"],"properties":{"id":{"type":"string","format":"uuid"},"text":{"type":"string"},"segmentId":{"type":"string","format":"uuid","nullable":true,"description":"The segment this prompt sits in. Optional on create — absent files it in the project's default segment, so a caller written before segments is unaffected."},"topic":{"type":"string","nullable":true,"description":"What the prompt is about. `category` is the name this field used to have and is still accepted on input; the column behind it is unchanged, and `category=` remains an accepted spelling of the filter."},"tags":{"type":"array","items":{"type":"string"}},"language":{"type":"string","nullable":true},"regions":{"type":"array","nullable":true,"items":{"type":"string"}},"llmsToQuery":{"type":"array","items":{"type":"string","enum":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"]}},"isActive":{"type":"boolean","nullable":true},"archivedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true}}}},"parameters":{"range":{"name":"range","in":"query","schema":{"type":"string","enum":["7d","14d","30d","90d","180d","365d"],"default":"30d"},"description":"A preset window ending today (UTC). A paired from/to wins over it."},"from":{"name":"from","in":"query","schema":{"type":"string","format":"date"},"description":"Inclusive UTC calendar day, YYYY-MM-DD. Must be paired with to."},"to":{"name":"to","in":"query","schema":{"type":"string","format":"date"},"description":"Inclusive UTC calendar day, YYYY-MM-DD. Must be paired with from."},"platform":{"name":"platform","in":"query","schema":{"type":"string"},"description":"Comma-separated llm_platform values to include; every platform measured otherwise."},"region":{"name":"region","in":"query","schema":{"type":"string"},"description":"Comma-separated region codes to include; every region measured otherwise."},"branded":{"name":"branded","in":"query","schema":{"type":"string","enum":["all","non_brand","branded"],"default":"all"},"description":"Which prompts the denominator is drawn from: those that name your brand, those that do not, or both. Never switched silently. `class` is a permanent alias of this parameter and both names are accepted; `class` is the older spelling and is not being removed."},"queryClass":{"name":"class","in":"query","schema":{"type":"string","enum":["all","non_brand","branded"],"default":"all"},"description":"Permanent alias of `branded`, accepted on every endpoint that takes it. Kept because it is in stored links, saved bookmarks and existing integrations; it is not deprecated and there is no removal date."},"granularity":{"name":"granularity","in":"query","schema":{"type":"string","enum":["day","week","month"],"default":"day"},"description":"Bucket for the series."},"topic":{"name":"topic","in":"query","schema":{"type":"string"},"description":"Narrow to one or more prompt topics, comma-separated; `category` is an accepted spelling. A prompt with no topic is filed under `Uncategorized` by every lens and matched under the same word here. A topic the project does not have matches nothing rather than widening to every topic."},"segment":{"name":"segmentId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Read this endpoint inside one segment of the project, as GET /api/segments returns its id. Absent means the whole project, which is what every figure here meant before segments existed. An id belonging to another project is 404 segment_not_found, never 403 — confirming it exists elsewhere would be the leak."},"projectId":{"name":"X-Project-Id","in":"header","required":false,"schema":{"type":"string","format":"uuid"},"description":"Project to scope the request to. If omitted, the user's default project is used. Can also be passed as ?projectId query parameter."}}},"security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"paths":{"/api/growth/findings/{id}/outcome":{"get":{"tags":["Growth"],"summary":"Observed publication outcome","description":"Equal 28-day windows anchored to confirmed public readback, excluding publication day plus three days reporting lag. Independent source coverage and confounds; not causal attribution.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"project","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Observed outcome, including publication state, windows, search, analytics, AI citations and confounds."},"401":{"description":"Authentication required."},"403":{"description":"Project read required."},"404":{"description":"Finding unavailable in this scope."}}}},"/api/growth/findings/{id}/check":{"post":{"tags":["Growth"],"summary":"Check a To do item now","description":"Project write; six per minute per person and project. Re-runs the project's stored-data analysis (no model or vendor call). `outcome` is `done` when the analysis moved the item to Done, `retired` when it retired an untouched New item, `still_observed` when it still observes it, and `not_checkable` for content work, an item with a brief, a closed item, or one the stored data cannot confirm. `sourceObservedAt` is when the item's source data was last refreshed, or null.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"project","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"{state, checkedAt, outcome, source, sourceObservedAt}."},"401":{"description":"Authentication required."},"403":{"description":"Project write required."},"404":{"description":"Finding unavailable in this scope."},"409":{"description":"The project's stored data could not be read (`analysis_unavailable`)."},"429":{"description":"More than six checks in a minute."}}}},"/api/growth/findings/{id}/learning":{"post":{"tags":["Growth"],"summary":"Propose finding-backed guidance","description":"Session editor only. Creates proposed learning with finding/run evidence references. Never accepts it automatically. A failed response is not an idempotent replay guarantee; inspect Knowledge before retry.","security":[{"cookieAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"project","in":"query","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","statement"],"properties":{"kind":{"type":"string","enum":["preference","fact","exception","covered_ground","rejected_topic"]},"statement":{"type":"string","minLength":1,"maxLength":4000}}}}}},"responses":{"201":{"description":"Proposed learning id, state, findingId and runId."},"400":{"description":"Invalid id, kind or statement."},"401":{"description":"Authentication required."},"403":{"description":"Signed-in project editor required; automation credentials refused."},"404":{"description":"Finding unavailable in this scope."},"429":{"description":"Write limit reached."}}}},"/api/growth":{"get":{"tags":["Growth"],"summary":"Read Growth findings","description":"The To do findings, ranked by priority band and then relative evidence score, with source coverage and decision history. AI visibility bands use a cohort-relative score when at least two prompts can be compared; the score orders review, not predicted traffic. `counts` are whole-queue state totals. Unknown lanes return 400 with validLanes; other invalid filters return an empty page.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"project","in":"query","description":"Project slug; alternatively use projectId or X-Project-Id.","schema":{"type":"string"}},{"name":"projectId","in":"query","description":"Project UUID, as an alternative to project or X-Project-Id.","schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","description":"One of open, new, in_progress, done, declined, suppressed. Omit for all states.","schema":{"type":"string","enum":["open","new","in_progress","done","declined","suppressed"]}},{"name":"kind","in":"query","description":"Filter by finding kind.","schema":{"type":"string","enum":["owned","earned","site","setup"]}},{"name":"lane","in":"query","description":"Filter by workstream. Unknown values return validLanes in a 400 response.","schema":{"type":"string","enum":["ai_visibility","search","site","setup"]}},{"name":"page","in":"query","description":"Positive page number, 25 findings per page. Default 1.","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"Stored result; measurements retain their own coverage and dates."},"400":{"description":"Invalid request."},"401":{"description":"Authentication required."},"403":{"description":"Project read access required. The body is { error, code, lock } with lock.reason = role."},"404":{"description":"Resource unavailable in this scope."}}}},"/api/growth/impact":{"get":{"tags":["Growth"],"summary":"Completed findings and what their published pages did afterwards","description":"Every finding in state done whose brief was published: the brief, the publication, the target prompts' mention rate 14 days before and after (a guarded comparison, refused with a reason where the periods are not comparable), the platforms citing the page, and markers for the accept and the publication. Reports and never attributes. A viewer may read it.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"project","in":"query","description":"Project slug; alternatively use projectId or X-Project-Id.","schema":{"type":"string"}}],"responses":{"200":{"description":"{ rows: ImpactRow[] }."},"401":{"description":"Authentication required."},"403":{"description":"Project read access required. The body is { error, code, lock } with lock.reason = role."}}}},"/api/growth/regenerate":{"post":{"tags":["Growth"],"summary":"Regenerate a written brief, three times a week","description":"Asks for a different brief for a week that was written, free, three times per project-week. Counted on opportunity_reports.regenerations_used (GET /api/projects/:id/opportunities returns report.regenerations); a fourth press is refused with the date the next scheduled edition resets the count. attemptId, when given, is the report's attemptId as the read returned it: a press naming a brief already replaced is refused with attempt_superseded before anything is bought. If the new attempt fails, the written brief is kept, the answer is kept, and the press still counts. A week we failed to write is POST /api/growth/retry's, which counts against none of these. Session only, editor only. The raw generator error never reaches the response.","security":[{"cookieAuth":[]}],"parameters":[{"name":"project","in":"query","description":"Project slug; alternatively use projectId or X-Project-Id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["weekStart"],"properties":{"weekStart":{"type":"string","description":"UTC Monday, YYYY-MM-DD."},"attemptId":{"type":"string","format":"uuid","description":"The brief shown: report.attemptId from GET /api/projects/:id/opportunities."}}}}}},"responses":{"200":{"description":"{ status: succeeded | kept | fallback | failed, weekStart, nextEditionAt }. kept: the new attempt failed and the written brief stands."},"400":{"description":"weekStart is not a UTC date, or attemptId is not a uuid."},"401":{"description":"Authentication required."},"403":{"description":"Editor grant required. The body is { error, code, lock } with lock.reason = role."},"409":{"description":"Refused: { error, code, nextEditionAt } where code is week_not_written (use /api/growth/retry), attempt_superseded, free_regenerate_used, project_inactive (the project is switched off), ai_not_configured, no_data or incomplete_evidence."},"429":{"description":"Rate limited."},"503":{"description":"Billing could not be verified."}}}},"/api/growth/retry":{"post":{"tags":["Growth"],"summary":"Try again on a week we failed to write, free","description":"Re-takes a failed or fallback week, or a claimed one stalled for more than ten minutes, and makes one more attempt. Every such failure is ours, so it is free and counts against none of the week's regenerations. attemptId is the report's attemptId as GET /api/projects/:id/opportunities returned it; the re-claim is guarded on it, so two presses of one attempt buy one attempt and the loser is refused with attempt_superseded. At most 3 presses per project-week per day. Session only, editor only. The raw generator error never reaches the response.","security":[{"cookieAuth":[]}],"parameters":[{"name":"project","in":"query","description":"Project slug; alternatively use projectId or X-Project-Id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["weekStart","attemptId"],"properties":{"weekStart":{"type":"string","description":"UTC Monday, YYYY-MM-DD."},"attemptId":{"type":"string","format":"uuid","description":"The report's attemptId as the read returned it."}}}}}},"responses":{"200":{"description":"{ status: succeeded | fallback | failed, weekStart, nextEditionAt }. A retry that fails again is a settled week and may be tried again."},"400":{"description":"weekStart is not a UTC date, or attemptId is not a UUID."},"401":{"description":"Authentication required."},"403":{"description":"Editor grant required. The body is { error, code, lock } with lock.reason = role."},"409":{"description":"Refused: { error, code, nextEditionAt } where code is week_not_failed, attempt_superseded, project_inactive (the project is switched off), ai_not_configured, no_data or incomplete_evidence."},"429":{"description":"Rate limited: 3 presses per project-week per day."},"503":{"description":"Billing could not be verified."}}}},"/api/growth/analyze":{"post":{"tags":["Growth"],"summary":"Analyze stored Growth evidence","description":"Reads stored sources only. Does not draft, publish or buy research. Same requestKey replays its saved run.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"project","in":"query","description":"Project slug; alternatively use projectId or X-Project-Id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"requestKey":{"type":"string","pattern":"^[a-zA-Z0-9_-]{1,100}$"}},"required":["requestKey"]}}}},"responses":{"200":{"description":"Stored result; measurements retain their own coverage and dates."},"400":{"description":"Invalid request."},"401":{"description":"Authentication required."},"403":{"description":"Project write access required."},"404":{"description":"Resource unavailable in this scope."},"409":{"description":"Request conflicts with current state."},"429":{"description":"Request limit reached."}}}},"/api/growth/findings/{id}":{"post":{"tags":["Growth"],"summary":"Record a Growth decision","description":"Acceptance creates one linked Content brief, or acknowledges a technical action. Dismiss, snooze and resolve require reason. Snooze requires a future timestamp.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"project","in":"query","description":"Project slug; alternatively use projectId or X-Project-Id.","schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"decision":{"type":"string","enum":["accept","dismiss","snooze","reopen","resolve"]},"reason":{"type":"string","maxLength":2000},"snoozedUntil":{"type":"string","format":"date-time"}},"required":["decision"]}}}},"responses":{"200":{"description":"Stored result; measurements retain their own coverage and dates."},"400":{"description":"Invalid request."},"401":{"description":"Authentication required."},"403":{"description":"Project write access required."},"404":{"description":"Resource unavailable in this scope."},"409":{"description":"Request conflicts with current state."},"429":{"description":"Request limit reached."}}}},"/api/growth/connectors":{"get":{"tags":["Growth"],"summary":"Read stored connector evidence","description":"Project-scoped Shopify Admin observations and Git-authored revisions. This read never contacts a provider; redirects remain configuration evidence until public crawl proof exists, and Git revisions are not deployment proof.","parameters":[{"name":"project","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Stored connector evidence and eligible Shopify destinations."},"401":{"description":"Authentication required."},"403":{"description":"Project read access required."}}},"post":{"tags":["Growth"],"summary":"Refresh Shopify Growth evidence","description":"Explicit bounded provider read. Requires connection-management authority and a Shopify destination in the active project. Replays update one latest observation rather than duplicating it.","parameters":[{"name":"project","in":"query","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["destinationId"],"properties":{"destinationId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Latest available, partial or unavailable observation."},"400":{"description":"Invalid destination id."},"401":{"description":"Authentication required."},"403":{"description":"Connection-management authority required."},"404":{"description":"Destination unavailable in this project scope."},"409":{"description":"Destination is not a supported Shopify destination."},"429":{"description":"Refresh limit reached."}}}},"/api/growth/strategy":{"get":{"tags":["Growth"],"summary":"Read Growth strategy and history","description":"Project-scoped goals, content calendar, success thresholds, known issues, open proposals and provenance-bearing revision history for the active brand.","parameters":[{"name":"project","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Resolved strategy, proposals, known issues and history."},"401":{"description":"Authentication required."},"403":{"description":"Project read access required."},"404":{"description":"No active brand is bound to this project."}}},"patch":{"tags":["Growth"],"summary":"Save a Growth strategy revision","description":"Project create authority. Saves explicit Growth fields as a new manual brand-profile revision. A 1-2,000 character rationale is required.","parameters":[{"name":"project","in":"query","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["values","rationale"],"properties":{"values":{"type":"object"},"rationale":{"type":"string","minLength":1,"maxLength":2000}}}}}},"responses":{"200":{"description":"Saved strategy revision."},"400":{"description":"Invalid strategy or missing rationale."},"401":{"description":"Authentication required."},"403":{"description":"Project create authority required."}}},"post":{"tags":["Growth"],"summary":"Propose or decide Growth context","description":"Proposes a known issue, decides an open strategy proposal, or accepts, rejects or supersedes a known issue. Decisions require a 1-2,000 character reason and stay scoped to the project's active brand.","parameters":[{"name":"project","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Decision recorded."},"201":{"description":"Known issue proposed."},"400":{"description":"Invalid action, transition or reason."},"401":{"description":"Authentication required."},"403":{"description":"Project create authority required."}}}},"/api/growth/demand":{"get":{"tags":["Growth"],"summary":"Read conventional keyword research","description":"Latest 20 conventional Google demand requests, states, results, provider task identity and cost. Not AI keyword volume.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"project","in":"query","description":"Project slug; alternatively use projectId or X-Project-Id.","schema":{"type":"string"}}],"responses":{"200":{"description":"Stored result; measurements retain their own coverage and dates."},"400":{"description":"Invalid request."},"401":{"description":"Authentication required."},"403":{"description":"Project read access required."},"404":{"description":"Resource unavailable in this scope."}}},"post":{"tags":["Growth"],"summary":"Collect conventional keyword demand","description":"Explicit paid-provider request. Requires Content subscription and reserves one Content unit. Same-day normalized seed/country/language replays. Uncertain dispatches keep their reservation and are not silently retried.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"project","in":"query","description":"Project slug; alternatively use projectId or X-Project-Id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"seed":{"type":"string","maxLength":250},"country":{"type":"string","minLength":2,"maxLength":2},"language":{"type":"string","minLength":2,"maxLength":2}},"required":["seed","country","language"]}}}},"responses":{"200":{"description":"Stored result; measurements retain their own coverage and dates."},"400":{"description":"Invalid request."},"401":{"description":"Authentication required."},"403":{"description":"Project write access required."},"404":{"description":"Resource unavailable in this scope."},"409":{"description":"Request conflicts with current state."},"429":{"description":"Request limit reached."}}}},"/api/prompts":{"get":{"tags":["Prompts"],"summary":"List prompts","description":"Return prompts in the active project, newest first. Optional brandId matches tracked IDs, names and aliases; paired from/to dates constrain mention results. Per-LLM status describes the selected brand, or your own brand by default, using the latest conclusive answer.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Array of prompt objects.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Prompt"}}}}},"401":{"description":"Unauthorized."}}},"post":{"tags":["Prompts"],"summary":"Create a prompt","description":"Create a new tracking prompt. The prompt will be queried against the configured LLMs on the next scheduled run.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","description":"The prompt text to track."},"topic":{"type":"string","nullable":true,"description":"Optional topic (e.g. 'product', 'support'). `category` is still accepted as the name this field used to have."},"tags":{"type":"array","items":{"type":"string"},"description":"Optional tags for filtering."},"language":{"type":"string","default":"en","description":"Language code."},"llmsToQuery":{"type":"array","items":{"type":"string","enum":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"]},"default":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"],"description":"LLM platforms to query."},"regions":{"type":"array","items":{"type":"string"},"nullable":true,"description":"ISO country codes the prompt runs in, e.g. [\"US\", \"GB\"]; UK resolves to GB. Omitted or empty inherits the project's target countries. A code outside the supported set is a 400 unsupported_region — never dropped, because a dropped list inherits the project and bills every country it targets."}}}}}},"responses":{"201":{"description":"Prompt created."},"400":{"description":"Validation error, prompt limit reached, or an unsupported region code (code unsupported_region, with the codes named in `unsupported`)."},"401":{"description":"Unauthorized."},"402":{"description":"Billing must be started first: no stored payment method (code no_payment_method) or no usable subscription (code billing_required). The body carries a billingUrl and a lock — { reason: plan | role, product: monitoring | content, canManage, ownerName? } — which says which product is locked and whether this reader can unlock it themselves. A 503 outage carries no lock, because no purchase fixes one."},"409":{"description":"The project already tracks this text (code duplicate_prompt); the body names the existing prompt."},"429":{"description":"Rate limited."},"503":{"description":"Billing could not be verified (code billing_unavailable); retry."}}}},"/api/prompts/bulk":{"post":{"tags":["Prompts"],"summary":"Create up to 100 prompts","description":"Validate and import one batch atomically. Existing active prompts are returned as already_tracked; new prompts are billed and scheduled together. Optional client UUIDs make retries idempotent when every row keeps the same content.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["prompts"],"properties":{"segmentId":{"type":"string","format":"uuid","nullable":true,"description":"Destination segment; omitted or null uses the project default."},"prompts":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["text"],"properties":{"id":{"type":"string","format":"uuid"},"text":{"type":"string"},"topic":{"type":"string","nullable":true},"category":{"type":"string","nullable":true,"description":"Permanent alias for topic."},"tags":{"type":"array","items":{"type":"string"}},"language":{"type":"string","default":"en"},"llmsToQuery":{"type":"array","items":{"type":"string","enum":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"]}},"regions":{"type":"array","items":{"type":"string"},"description":"Supported ISO country codes; omitted or empty inherits project countries."},"source":{"type":"string","enum":["manual","csv","suggested","fanout","keyword"]},"suggestionId":{"type":"string","format":"uuid"},"topicKeyword":{"type":"string","nullable":true}}}}}}}}},"responses":{"201":{"description":"All requested prompts, the total count, duplicate count and one created or already_tracked outcome per input row."},"400":{"description":"Invalid, empty or oversized batch, unsupported region, or no active project."},"401":{"description":"Unauthorized."},"402":{"description":"Monitoring billing or a payment method is required."},"403":{"description":"Project write access or an active organization is required."},"404":{"description":"Destination segment not found."},"409":{"description":"Prompt ID, text or suggestion conflicts with current project state."},"429":{"description":"Rate limit exceeded."},"503":{"description":"Billing could not be verified or updated."}}}},"/api/prompts/cost-preview":{"post":{"tags":["Prompts"],"summary":"Preview the billing effect of a prompt change","description":"Preview adding prompts, changing prompt or project regions, or restoring and resuming prompts. Members without billing permission receive only the proposed change; billing managers also receive account totals, payment state and the Stripe invoice preview. In a switched-off project every change prices at zero and the response carries projectSwitchedOff: true; a restore, resume or region preview there needs no card or subscription, because the write is not gated on billing.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prompts":{"type":"array","minItems":1,"maxItems":10000,"items":{"type":"object","properties":{"regions":{"type":"array","items":{"type":"string"}}}},"description":"Prompts proposed for creation."},"promptIds":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string","format":"uuid"},"description":"Existing prompts affected by a region or lifecycle change."},"regions":{"type":"array","items":{"type":"string"}},"projectRegions":{"type":"array","items":{"type":"string"},"description":"Proposed project countries; previews all active prompts."},"lifecycle":{"type":"string","enum":["resume","restore"]},"llmsToQuery":{"type":"array","items":{"type":"string","enum":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"]},"description":"Platforms used when resuming or restoring prompts."}}}}}},"responses":{"200":{"description":"Change counts and monthly delta. Billing managers also receive resulting totals and invoice/payment fields; previewUnavailable says whether Stripe produced a usable preview."},"400":{"description":"Invalid prompt, region or lifecycle selection."},"401":{"description":"Unauthorized."},"403":{"description":"An active organization is required."},"404":{"description":"Project, billing account or selected prompt not found."},"409":{"description":"Prompt lifecycle or platform configuration prevents the change."},"429":{"description":"Rate limit exceeded."}}}},"/api/prompts/{id}":{"get":{"tags":["Prompts"],"summary":"Get prompt by ID","description":"The prompt with its own period figures — mention rate, cited rate over the answers whose sources were observable, mean position and platform coverage, each compared on the platforms and regions both periods measured — the newest answer per platform and region, the brands ranked on it and what changed, read through the same range and filters as the dashboard. The answers themselves are GET /api/results?promptId=.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/range"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/platform"},{"$ref":"#/components/parameters/region"},{"$ref":"#/components/parameters/granularity"}],"responses":{"200":{"description":"Prompt object."},"400":{"description":"Malformed range."},"404":{"description":"Not found."}}},"put":{"tags":["Prompts"],"summary":"Update a prompt","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"},"category":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"isActive":{"type":"boolean"},"llmsToQuery":{"type":"array","items":{"type":"string","enum":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"]}},"regions":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Region codes for this prompt. Null or an empty array inherits the project defaults."},"archived":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated prompt."},"404":{"description":"Not found."}}},"delete":{"tags":["Prompts"],"summary":"Delete a prompt","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted."},"404":{"description":"Not found."}}}},"/api/prompts/{id}/run":{"post":{"tags":["Prompts"],"summary":"Run an active prompt now","description":"Schedule the prompt's manual platform-region round. One manual round is available per prompt every seven days across REST and MCP. A usable account subscription must cover every active prompt-region unit. A prompt in a switched-off project is refused with `project_inactive`. The response is accepted before background queries finish.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"Queries scheduled.","content":{"application/json":{"schema":{"type":"object","properties":{"scheduled":{"type":"integer"},"llms":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"No active project, the prompt is inactive or archived, or its project is switched off (`project_inactive`)."},"401":{"description":"Unauthorized."},"402":{"description":"Billing is not active or does not cover the account allocation."},"404":{"description":"Prompt not found."},"429":{"description":"The seven-day manual allowance is not available."},"503":{"description":"Stripe entitlement could not be verified."}}}},"/api/prompts/bulk-archive":{"post":{"tags":["Prompts"],"summary":"Archive or restore prompts","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string","format":"uuid"}},"archive":{"type":"boolean","default":true,"description":"False restores the selected prompts."}}}}}},"responses":{"200":{"description":"Number of matching prompts updated and the requested archive state."},"400":{"description":"Invalid or oversized ID list, or no active project."},"401":{"description":"Unauthorized."},"429":{"description":"Rate limit exceeded."}}}},"/api/projects":{"get":{"tags":["Projects"],"summary":"List projects","description":"Return the active organization's projects visible to the caller, narrowed by project grants and API-key scope.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Array of project objects."},"401":{"description":"Unauthorized."}}},"post":{"tags":["Projects"],"summary":"Create a project","description":"Create a project with a default segment and own brand. Optional creationId replays the same normalized intent with 200, including at the project ceiling. A changed intent or an ID used outside this organization returns a generic 409. New projects use answer-based `unique_results` share of voice. Every account has the same 999-project safety ceiling.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","domain"],"properties":{"name":{"type":"string","description":"Project name."},"domain":{"type":"string","description":"Website domain (e.g. example.com)."},"domains":{"type":"array","items":{"type":"string"},"description":"Additional own-brand domains; normalized and deduplicated with the primary domain first."},"targetCountries":{"type":"array","items":{"type":"string"},"description":"Supported country codes; defaults to US when no supported country is supplied."},"brandGroupId":{"type":"string","format":"uuid","nullable":true,"description":"An active client group in this organization."},"creationId":{"type":"string","format":"uuid","description":"Optional stable project UUID for retrying the same creation intent."}}}}}},"responses":{"200":{"description":"Same creationId and normalized intent replayed; existing project returned."},"201":{"description":"Project, default segment and own brand created."},"400":{"description":"Validation error."},"401":{"description":"Unauthorized."},"403":{"description":"API-key scope forbids creation, or the 999-project account ceiling was reached (`project_cap_reached`)."},"404":{"description":"Client group not found in this organization."},"409":{"description":"creation_id_conflict — this creation ID is already in use for a different intent or organization."}}}},"/api/projects/bulk":{"post":{"tags":["Projects"],"summary":"Create up to 25 projects in one call","description":"Bulk-create projects, each optionally including inline brands and prompts. New projects use answer-based `unique_results` share of voice. Designed for onboarding scripts. Returns both `created` and `errors` arrays — partial success returns 207. Cookie and API-key callers share the 999-project account ceiling, and inline prompts accept only the seven queryable platforms.","security":[{"apiKeyAuth":[]},{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["projects"],"properties":{"projects":{"type":"array","maxItems":25,"items":{"type":"object","required":["name","domain"],"properties":{"name":{"type":"string"},"domain":{"type":"string"},"brands":{"type":"array","items":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"domain":{"type":"string","nullable":true},"aliases":{"type":"array","items":{"type":"string"}},"isCompetitor":{"type":"boolean"}}}},"prompts":{"type":"array","maxItems":200,"items":{"type":"object","required":["text"],"properties":{"text":{"type":"string"},"topic":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"language":{"type":"string"},"llmsToQuery":{"type":"array","items":{"type":"string","enum":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"]}}}}}}}}}}}}},"responses":{"201":{"description":"All projects created."},"207":{"description":"Partial success — some projects created, some failed. See errors[]."},"400":{"description":"All projects failed validation, a project exceeded 200 prompts (`prompt_cap`), a prompt used an unsupported platform (`unsupported_platforms`), or the batch was too large."},"401":{"description":"Unauthorized."},"403":{"description":"API-key scope forbids creation, or the 999-project account ceiling was reached (`project_cap_reached`)."}}}},"/api/api-keys/me":{"get":{"tags":["API Keys"],"summary":"Introspect the calling API key","description":"Returns the calling key's scope, access mode, expiry, creation permission, the seven allowed LLM platforms, and visible projects. Lets scripts validate before doing destructive ops. A read-only key may call it.","security":[{"apiKeyAuth":[]}],"responses":{"200":{"description":"Key context.","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"object","properties":{"scope":{"type":"string","enum":["all","selected"]},"accessMode":{"type":"string","enum":["read","write"]},"canCreateProjects":{"type":"boolean"},"expiresAt":{"type":"string","format":"date-time","nullable":true}}},"user":{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}},"capabilities":{"type":"object","properties":{"allowedPlatforms":{"type":"array","items":{"type":"string","enum":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"]}}}},"projects":{"type":"array","description":"Owned projects narrowed to the calling key's scope.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"}}}}}}}}},"401":{"description":"Not an API-key authed request."}}}},"/api/projects/{id}":{"get":{"tags":["Projects"],"summary":"Get project by ID","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Project object."},"404":{"description":"Not found."}}},"patch":{"tags":["Projects"],"summary":"Update a project","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"domain":{"type":"string","nullable":true},"logoUrl":{"type":"string","nullable":true},"isActive":{"type":"boolean","description":"Switches the project on or off. A switched-off project is neither measured nor billed; switching on with active prompts raises the subscription quantity and needs a usable subscription. Any isActive over active prompts is a billing write, including re-sending the current value: true is gated, and either value syncs the quantity."}}}}}},"responses":{"200":{"description":"Updated project; `billingSyncPending: true` when a quantity decrease could not reach Stripe."},"400":{"description":"Validation failure, including a non-boolean `isActive`."},"402":{"description":"Billing required to switch the project on or add billed regions."},"404":{"description":"Not found."},"503":{"description":"Billing unavailable; the request's changes were restored, with `billingSyncPending: true` when the quantity could not be synced back."}}},"delete":{"tags":["Projects"],"summary":"Delete a project","description":"Delete a project. Cannot delete the default project.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted."},"400":{"description":"Cannot delete default project."},"404":{"description":"Not found."}}}},"/api/projects/{id}/products":{"get":{"tags":["Projects"],"summary":"Read a catalogue segment's products","description":"Coverage, pooled figures, source sync states and paged product rows. Each row's catalogFacts lists current supported facts as key, label, expected and source (catalogue or manual); historical factualChecks retain their original expected snapshots. Rates use completed exact conclusive reads and explicit prompt applicability; null means unknown and zero means analysed absence. `segmentId` is required. Row search/filtering does not change headline totals.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"segmentId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}},{"name":"platform","in":"query","schema":{"type":"string"}},{"name":"region","in":"query","schema":{"type":"string"}},{"name":"q","in":"query","schema":{"type":"string","maxLength":200}},{"name":"issue","in":"query","schema":{"type":"string","enum":["all","measured_omission","price_mismatch","listing_restriction","description_gap","analysis_gap"]}},{"name":"sort","in":"query","schema":{"type":"string","enum":["issues_desc","visibility_desc","win_rate_desc","title_asc"]}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"Catalogue, tiles and product rows."},"400":{"description":"No segmentId, or a bad range."},"404":{"description":"Segment not found, including one belonging to another project."},"409":{"description":"That segment is not a catalogue segment."}}},"patch":{"tags":["Projects"],"summary":"Set or clear a product's reference facts","description":"Requires project write access. Manual overrides survive catalogue sync. Null clears an override and restores its retained connected value when available. Saving buys no model call and does not change historical answer checks.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["segmentId","productId"],"description":"Set or clear at least one of turnaround, biomarkerCount, sampleType or fastingRequired. A manual reference URL must use the product's hostname.","properties":{"segmentId":{"type":"string","format":"uuid"},"productId":{"type":"string","format":"uuid"},"turnaround":{"type":"string","nullable":true,"description":"Positive integer hour/day value or range, optionally working, business or calendar; for example 2-3 working days or 24 hours."},"biomarkerCount":{"type":"integer","minimum":1,"maximum":9007199254740991,"nullable":true},"sampleType":{"type":"string","enum":["blood","urine","saliva","stool","hair","swab"],"nullable":true},"fastingRequired":{"type":"boolean","nullable":true},"referenceUrl":{"type":"string","format":"uri","maxLength":2000,"nullable":true}}}}}},"responses":{"200":{"description":"productId, current effective facts (key, label, expected and source), spent: false and analysisUpdated: false."},"400":{"description":"Invalid body, identifier or reference fact."},"401":{"description":"Unauthorized."},"403":{"description":"Project or credential is read-only."},"404":{"description":"Scoped product or catalogue segment not found."},"409":{"description":"Not a catalogue segment, or fact_capacity: the bounded attributes cannot retain both restrictions and manual facts."},"429":{"description":"Rate limit exceeded."}}}},"/api/projects/{id}/products/catalogue":{"get":{"tags":["Projects"],"summary":"Read project-wide catalogue facts","description":"The product facts Content uses across every catalogue segment in the project. No measurement fields or monitoring purchase. Every product retains its catalogue segment; attributes is always an array. monitored means at least one prompt is active and unarchived, and controls only whether Knowledge links to the measured Products page.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Catalogue segments, project-wide product facts and monitoring availability.","content":{"application/json":{"schema":{"type":"object","required":["segments","products","monitored"],"properties":{"segments":{"type":"array","items":{"type":"object","required":["id","name","slug","kind"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"kind":{"type":"string","enum":["catalog"]}}}},"products":{"type":"array","items":{"type":"object","required":["id","segmentId","source","title","collection","priceCents","currency","availability","url","attributes","monitoringRole"],"properties":{"id":{"type":"string","format":"uuid"},"segmentId":{"type":"string","format":"uuid"},"source":{"type":"string","enum":["csv","shopify","merchant_center"]},"monitoringRole":{"type":"string","enum":["primary","add_on"],"nullable":true},"title":{"type":"string"},"collection":{"type":"string","nullable":true},"priceCents":{"type":"integer","nullable":true},"currency":{"type":"string","nullable":true},"availability":{"type":"string","nullable":true},"url":{"type":"string","nullable":true},"attributes":{"type":"array","items":{"type":"object","additionalProperties":true}}}}},"monitored":{"type":"boolean"}}}}}},"404":{"description":"Project not found."}}},"patch":{"operationId":"patch_projects_id_products_catalogue","tags":["Products"],"summary":"Classify catalogue products","description":"Mark 1–100 active catalogue products as primary offerings, add-ons, or unclassified (null). Classification survives source imports, informs suggestions and can filter product measurements. This buys no model work and never invents facts.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"productIds":{"minItems":1,"maxItems":100,"type":"array","items":{"type":"string","minLength":1,"format":"uuid"}},"monitoringRole":{"nullable":true,"type":"string","enum":["primary","add_on"]}},"required":["productIds","monitoringRole"]}}}},"responses":{"200":{"description":"Updated products and their monitoring role; spent false."},"400":{"description":"Invalid or duplicate product IDs or unsupported role."},"401":{"description":"Unauthorized."},"403":{"description":"Project is read-only or outside credential scope."},"404":{"description":"One or more active catalogue products were not found; no update occurred."},"429":{"description":"Rate limit exceeded."}}}},"/api/projects/{id}/products/import":{"post":{"tags":["Projects"],"summary":"Import a catalogue from a CSV","description":"Spends nothing. Idempotent on (segment, external id): re-importing updates in place, and products the file omits are left alone rather than deleted. A line that cannot be read is refused by line and column and the rest still lands.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["csv"],"properties":{"segmentId":{"type":"string","format":"uuid","description":"Optional catalogue destination. Omit to create one after CSV validation when none exists, use the only one, or receive catalog_selection_required with the available segments when several exist."},"csv":{"type":"string","description":"The file's text, at most 2 MB."}}}}}},"responses":{"200":{"description":"Counts created, updated and untouched, plus refusals."},"400":{"description":"No csv, an empty file, or no id column."},"403":{"description":"This project is read-only for you."},"409":{"description":"That segment is not a catalogue, or { error: 'Choose which catalogue should receive this import.', code: 'catalog_selection_required', segments } when no segmentId was supplied and several exist."},"413":{"description":"The file is larger than 2 MB."}}}},"/api/projects/{id}/products/prompt-scopes":{"get":{"tags":["Projects"],"summary":"Read explicit product applicability","description":"Requires segmentId. Returns active prompts and saved scopes, collections and bounded product options searched by q and paged by offset (50 per response).","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"segmentId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"q","in":"query","schema":{"type":"string","maxLength":200}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Prompts, collection and product options, catalogue count."}}},"patch":{"tags":["Projects"],"summary":"Save explicit product applicability","description":"Editor access. Body: segmentId, promptIds and scope (null, whole_catalog, collections with collections[], or products with productIds[]). Selectors must belong to this catalogue. Atomic; buys no calls or prompts.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["segmentId","promptIds","scope"],"properties":{"segmentId":{"type":"string","format":"uuid"},"promptIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"scope":{"type":["object","null"]}}}}}},"responses":{"200":{"description":"Saved scopes, configured state, spent false."},"400":{"description":"Invalid scope or selectors."},"403":{"description":"Project is read-only."},"404":{"description":"Prompt or catalogue unavailable."}}}},"/api/projects/{id}/products/match":{"post":{"tags":["Projects"],"summary":"Match products to stored answers (billable)","description":"The one metered thing about a catalogue. The `product_matching` allowance is reserved before the first model call, so a control can print its price first; verdicts are stored, so a second call re-reads no answer. A request sends at most 320,000 characters of model input across its reads, so a large catalogue reads fewer answers a request. answersWaiting includes the whole remaining backlog and failures; exact generation fingerprints permit rereading changed answers.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"segmentId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"What the pass read, and whether this call spent the allowance or replayed a stored result."},"402":{"description":"The allowance needs a plan."},"403":{"description":"This project is read-only for you."},"409":{"description":"A pass is already running, or the segment is not a catalogue."},"422":{"description":"The catalogue's product list is too long to send beside one answer; nothing is reserved."},"429":{"description":"The allowance is used up for this window."}}}},"/api/projects/{id}/measure":{"get":{"tags":["Projects"],"summary":"What a round would cost, and where each platform stands","description":"The state the header's measurement control renders before anybody presses it: every platform the project's active prompts ask for with its own state, the platform calls one round buys, and the round already in flight if there is one. It is not a gate — POST is — but a control that already knows the answer never has to produce a refusal to find it out. A viewer may read it and is told canRun: false.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{ projectId, canRun, switchedOff, running, prompts, calls, platforms[] }. switchedOff is true for a switched-off project, which POST refuses, so canRun is false there. Each platform state is measuring, answered, gap (asked and produced no measurement, which is never a zero) or never, with coverage: its last 30 days as { days, expected, received, inFlight, collapsed } from the round targets — collapsed when it received under half of at least 8 expected answers — or null when no round in the window asked it."},"401":{"description":"Unauthorized."},"404":{"description":"Project not found or not visible to the caller."}}},"post":{"tags":["Projects"],"summary":"Measure this project now (billable)","description":"Opens the same measurement rounds and sends the same query/scheduled events the daily fan-out sends, one round per prompt and region, with trigger 'manual'. No second dispatcher and no second cost path: llm_calls, the gen_ai span and query_results are written by run-llm-query exactly as on a scheduled round. A project outside the caller's companies answers 404 before any role or billing check, because a 403 would confirm it exists.","security":[{"cookieAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"202":{"description":"{ scheduled, rounds } — events sent and rounds opened."},"401":{"description":"Unauthorized."},"402":{"description":"billing_required with the shared { error, code, lock } body, or active_allocation_required with no lock when the account pays and this project has no active prompt."},"403":{"description":"project_read_only with the shared lock body."},"404":{"description":"Project not found or not visible to the caller."},"409":{"description":"round_in_flight, carrying the running round, rather than buying every platform twice; no_queryable_platforms; or project_inactive when the project is switched off."},"429":{"description":"Ten hand-started rounds an hour."},"503":{"description":"measurement_dispatch_unknown — the send was not confirmed. A retry re-derives the same correlation and re-uses the round already opened."}}}},"/api/brands":{"get":{"tags":["Brands"],"summary":"List tracked brands","description":"Return all tracked brands (own brand and competitors) for the active project. Each row carries its lifetime mention rate and the counts it is a ratio of; there is no composite score.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Array of brand objects.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Brand"}}}}},"401":{"description":"Unauthorized."}}},"post":{"tags":["Brands"],"summary":"Add a tracked brand","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"domain":{"type":"string","nullable":true},"aliases":{"type":"array","items":{"type":"string"},"description":"Alternative names for matching."},"isOwnBrand":{"type":"boolean","default":false},"isCompetitor":{"type":"boolean","default":false}}}}}},"responses":{"200":{"description":"The brand the project already has. isOwnBrand: true updates the single own-brand slot in place; a competitor whose name the project already tracks is returned as it stands (alreadyTracked: true) rather than duplicated."},"201":{"description":"Brand created."},"400":{"description":"Validation error or competitor limit reached."},"401":{"description":"Unauthorized."}}}},"/api/brands/page":{"get":{"tags":["Brands"],"summary":"The whole Brands screen, in one request","description":"The roster under the selected range, the share-of-voice block, the stacked share bands and the discovery strip, assembled on the server rather than joined in the browser. This used to be a query flag on GET /api/brands; a flag that changes the response SHAPE is two contracts at one address, so the page moved here and /api/brands answers the roster for every query.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"{ brands, shareOfVoice, shareSeries, segmentShareDeltas, discovered, range, previousRange, mentionCountMode, facets? }. The figures are the whole range's; every delta is taken on the cells the Overview's mention-rate comparison was made on, carries its basis and likeForLike, and is refused whenever that comparison is. The own brand's rate skips relevance-gated mentions, so it equals the Overview tile. Each discovered name carries kind (competitor, retailer, public_body, tool, other, own_product or null), aliasOf and groupHeadId."},"400":{"description":"No active project."},"401":{"description":"Unauthorized."},"404":{"description":"segment_not_found."},"429":{"description":"Rate limited."}}}},"/api/brands/{id}":{"get":{"tags":["Brands"],"summary":"Get a tracked brand","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Brand object."},"400":{"description":"No active project."},"401":{"description":"Unauthorized."},"404":{"description":"Brand not found."}}},"put":{"tags":["Brands"],"summary":"Update a tracked brand","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"domain":{"type":"string","nullable":true},"aliases":{"type":"array","items":{"type":"string"}},"isOwnBrand":{"type":"boolean"},"isCompetitor":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated brand."},"400":{"description":"Invalid JSON or no active project."},"401":{"description":"Unauthorized."},"404":{"description":"Brand not found."},"409":{"description":"Refused: demoting the own brand (code own_brand_required), a name another brand in the project already uses (code brand_name_taken), or promoting a second own brand (code own_brand_exists)."}}},"delete":{"tags":["Brands"],"summary":"Delete a tracked brand","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Brand deleted."},"400":{"description":"No active project."},"401":{"description":"Unauthorized."},"404":{"description":"Brand not found."},"409":{"description":"Refused: this is the project's own brand, which every result is scored against (code own_brand_required). Rename it instead."}}}},"/api/analytics/llm-traffic":{"get":{"tags":["Dashboard"],"summary":"Read AI-referred GA4 traffic","description":"Returns the selected project's AI-referred sessions, revenue (in the property's own currency), landing pages, assistant series and equal-window deltas over the settled days of the range (to the newest day GA4 has finished counting, three days before today). The selected GA4 key event is measured separately as the share of AI-referred sessions that reached it; it is null when no event is selected or Google cannot measure it. Deltas are first_period when the previous window recorded no session and refused (no_baseline) when it could not be read. Uses cached daily traffic when it covers the window, but an event-specific read still contacts GA4.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"projectId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/granularity"},{"name":"startDate","in":"query","schema":{"type":"string"},"description":"Legacy GA4 date expression used only when from is absent; calendar days and relative expressions such as 30daysAgo are accepted."},{"name":"endDate","in":"query","schema":{"type":"string","default":"today"},"description":"Legacy GA4 end-date expression paired with startDate."}],"responses":{"200":{"description":"Traffic summary, or { connected: false } when the project has no active GA4 integration.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/LlmTrafficResponse"},{"type":"object","required":["connected"],"properties":{"connected":{"type":"boolean","enum":[false]}}}]}}}},"400":{"description":"Missing or malformed projectId, or an invalid date range."},"401":{"description":"Unauthorized."},"404":{"description":"Project unavailable in the caller's scope."},"409":{"description":"The GA4 connection has no selected property."},"502":{"description":"Google Analytics could not be read."}}}},"/api/dashboard/overview":{"get":{"tags":["Dashboard"],"summary":"Get the measured overview","description":"One measured cohort for the active project: the own brand's mention rate with counts and interval, cited rate, mean position, sentiment, platform coverage, a series per bucket, per-platform slices, a comparison with the previous period that says when the two are not comparable, brand rankings each carrying their own delta and series, share of voice, and the Overview block: tiles (mention rate, cited rate over the answers whose sources were observable, rank and prompts naming you, each with a guarded PeriodDelta and a bucket Spark; rank and promptsNaming also carry now, the whole period's figure to print, and of — for the rank, the size of the selected segment's roster), seriesBy (the same figure grouped by platform, brand, topic or segment), defaultSeries, buckets (the segments table: a row per segment with its topics nested, each segment row carrying the own brand's rank and share of voice inside that segment's roster), matrix (buckets against platforms, refused above 210 cells rather than truncated) and segmentCount, plus recent alerts. Every delta is made like for like, on the prompt × platform × region cells both periods measured with any collapsed platform left out, and then carries basis \"like_for_like\" and likeForLike; it is refused as insufficient_overlap when those cells hold under 70% of the current answers. The own brand's ranking row and the mention-rate tile skip the same relevance-gated mentions. Every rate carries its denominator; nothing here is a composite score or an average of percentages.","parameters":[{"$ref":"#/components/parameters/range"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/platform"},{"$ref":"#/components/parameters/region"},{"$ref":"#/components/parameters/topic"},{"$ref":"#/components/parameters/branded"},{"$ref":"#/components/parameters/queryClass"},{"$ref":"#/components/parameters/granularity"},{"$ref":"#/components/parameters/segment"}],"responses":{"200":{"description":"Dashboard overview.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/MeasuredCohort"},{"type":"object","properties":{"avgSentiment":{"type":"number","nullable":true,"description":"Sentiment on the most recent rolled-up day. Not the quantity sentimentTrend is a trend of — for the cohort's own figure read metrics.sentiment.score."},"sentimentTrend":{"type":"number","nullable":true,"description":"Change in the cohort's sentiment against the PREVIOUS EQUAL-LENGTH PERIOD, over the same eligibility, prompt basket and filters as every other figure here. It compared the previous rolled-up day until September 2026. Null when either period measured no sentiment. Sentiment is a mean, not a proportion, so it carries no interval."},"unreadAlerts":{"type":"integer"},"activePrompts":{"type":"integer"},"trackedBrands":{"type":"integer"},"recentAlerts":{"type":"array","items":{"type":"object"}}}}]}}}},"400":{"description":"No active project, or an unpaired, reversed or non-calendar from/to."},"401":{"description":"Unauthorized."},"429":{"description":"Rate limit exceeded."}}}},"/api/projects/{id}/trends":{"get":{"tags":["Dashboard"],"summary":"The measured block plus category buckets and recent activity","description":"The same measured block and Overview block as the overview for one project, plus promptBuckets (per-category mention rates over all the category's answers, each with a PeriodDelta compared like for like and a bucket series; the bare change field was removed in September 2026 — read delta.verdict and delta.reason), recentActivity (the ten newest alerts), the granularity, the range, available (the platforms and regions the filters can be set to) and viewer.canWriteProject.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/range"},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"},{"$ref":"#/components/parameters/platform"},{"$ref":"#/components/parameters/region"},{"$ref":"#/components/parameters/topic"},{"$ref":"#/components/parameters/branded"},{"$ref":"#/components/parameters/queryClass"},{"$ref":"#/components/parameters/granularity"},{"$ref":"#/components/parameters/segment"}],"responses":{"200":{"description":"Measured trends.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/MeasuredCohort"},{"type":"object","properties":{"promptBuckets":{"type":"array","items":{"type":"object"}},"recentActivity":{"type":"array","items":{"type":"object"}},"granularity":{"type":"string","enum":["day","week","month"]},"available":{"type":"object","properties":{"platforms":{"type":"array","items":{"type":"string"}},"regions":{"type":"array","items":{"type":"string"}}}},"viewer":{"type":"object","properties":{"canWriteProject":{"type":"boolean"}}}}}]}}}},"400":{"description":"Invalid date range."},"401":{"description":"Unauthorized."},"403":{"description":"Key not scoped to this project."},"404":{"description":"Project not found."}}}},"/api/projects/{id}/fanout":{"get":{"tags":["Dashboard"],"summary":"The searches the platforms ran","description":"The searches the platforms ran to answer this project's prompts, read from the capture fields on each stored answer and never inferred: coverage per exposure state and the top observed queries. A platform that does not expose its searches is reported as unknown, never as zero. Promotion of a query to a tracked prompt is not done here and never automatic.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"promptId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"segmentId","in":"query","schema":{"type":"string"},"description":"A segment id or slug (alias segment); narrows every figure to its prompts."},{"name":"from","in":"query","schema":{"type":"string"},"description":"YYYY-MM-DD (the whole UTC day) or an ISO timestamp. Paired with to; the last 30 days otherwise."},{"name":"to","in":"query","schema":{"type":"string"},"description":"YYYY-MM-DD (through the end of that UTC day) or an ISO timestamp."},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Fan-out read.","content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}}},"promptId":{"type":"string","nullable":true},"coverage":{"type":"object","properties":{"totalAnswers":{"type":"integer"},"answersWithUnknownSearches":{"type":"integer"},"states":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string","enum":["not_exposed","not_searched","exposed_empty","exposed","unknown"]},"label":{"type":"string"},"searchesKnown":{"type":"boolean"},"meaning":{"type":"string"},"answers":{"type":"integer"},"platforms":{"type":"array","items":{"type":"string"}},"observedQueries":{"type":"integer","nullable":true,"description":"Null wherever the provider told us nothing."}}}}}},"topSearches":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string"},"occurrences":{"type":"integer"},"promptCount":{"type":"integer"},"platforms":{"type":"array","items":{"type":"string"}},"engines":{"type":"array","items":{"type":"string"}},"lastSeenAt":{"type":"string","format":"date-time","nullable":true},"brand":{"type":"object","description":"How the eligible answers that ran this search treated the own brand, counted per answer. citedRate is observed-only: null with observed 0 means not observable, never 0%.","properties":{"answers":{"type":"integer"},"named":{"type":"integer"},"observed":{"type":"integer"},"cited":{"type":"integer"},"namedRate":{"type":"number","nullable":true},"citedRate":{"type":"number","nullable":true}}}}}},"promotion":{"type":"object","description":"automatic: false, and the cost-preview and create routes a caller uses instead."}}}}}},"400":{"description":"Malformed id or date range."},"401":{"description":"Unauthorized."},"403":{"description":"Key not scoped to this project."},"404":{"description":"Project not found."},"429":{"description":"Rate limit exceeded."}}}},"/api/projects/{id}/opportunities":{"get":{"tags":["Dashboard"],"summary":"The stored weekly brief","description":"The stored weekly opportunity brief for a project. This route never generates one: the brief is bought once a week by the scheduled job and nowhere else. Findings carry observation, action and links resolved server-side from stored ids; the model that wrote the brief produced no URLs.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"weekStart","in":"query","schema":{"type":"string","format":"date"},"description":"A stored week's Monday (UTC). The newest stored week otherwise."}],"responses":{"200":{"description":"Brief read.","content":{"application/json":{"schema":{"type":"object","properties":{"projectId":{"type":"string","format":"uuid"},"report":{"type":"object","nullable":true,"description":"id, weekStart, status (claimed, succeeded, fallback, failed), generatedAt, model, metricVersion, scorerVersion, superseded, supersededReason, evidencePeriod, summary, findings, evidence, usage, error. superseded is true on a written edition stamped with an older metric or scorer version than the current one. A fallback edition is written from the stored evidence with no model; its model reads deterministic."},"dataFreshness":{"type":"object","properties":{"latestAnswerAt":{"type":"string","nullable":true},"ageHours":{"type":"number","nullable":true},"stale":{"type":"boolean"}}},"nextStep":{"type":"object","properties":{"code":{"type":"string","enum":["ready","no_brief_yet","in_flight","fallback_this_week","failed_this_week"]},"message":{"type":"string"}}},"availableWeeks":{"type":"array","items":{"type":"string","format":"date"}}}}}}},"400":{"description":"Malformed id or weekStart."},"401":{"description":"Unauthorized."},"403":{"description":"Key not scoped to this project."},"404":{"description":"Project not found."}}}},"/api/projects/{id}/cited-visited":{"get":{"tags":["Dashboard"],"summary":"Compare observed citations with attributable AI visits","description":"Observed citations beside attributable GA4 sessions and key events by assistant and landing page. The association does not establish that a citation caused a visit. Unavailable attribution is null with a reason, never zero. gaCompleteness discloses folded, thresholded or sampled page data; partial stored attribution is a lower bound.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"platform","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Citation and attributable-visit rows for the selected range."},"400":{"description":"Invalid or missing range."},"401":{"description":"Unauthorized."},"404":{"description":"Project not found."}}}},"/api/projects/{id}/analytics-diagnostic":{"get":{"tags":["Dashboard"],"summary":"Explain a stored GA4 landing-page total","description":"Breaks one exact landing page into stored GA4 country, hostname, source/medium, channel and device dimensions. No Google call is made. A pending dimension backfill marks older rows as provisional; GA4 sessions and Search Console clicks use different attribution systems.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"landingPage","in":"query","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"Stored page totals, dimension backfill state and bounded breakdowns."},"400":{"description":"Invalid page, dates or range over 90 days."},"401":{"description":"Unauthorized."},"403":{"description":"Key not scoped to this project."},"404":{"description":"Project not found."},"409":{"description":"Project domain or GA4 connection unavailable."}}}},"/api/projects/{id}/ai-visits":{"get":{"tags":["Dashboard"],"summary":"Pageviews the AI traffic pixel observed","description":"Pageviews the project's traffic pixel recorded, and nothing that could be built into a visitor, a session or a conversion. The range must sit inside the 30-day retention window.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"$ref":"#/components/parameters/from"},{"$ref":"#/components/parameters/to"}],"responses":{"200":{"description":"Pixel pageviews.","content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"string","enum":["ai_pixel"]},"range":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}}},"retentionDays":{"type":"integer"},"pixel":{"type":"object","properties":{"configured":{"type":"boolean"},"activeTokens":{"type":"integer"},"lastSeenAt":{"type":"string","nullable":true}}},"latestVisitAt":{"type":"string","nullable":true},"totals":{"type":"object","properties":{"pageviews":{"type":"integer"},"aiPageviews":{"type":"integer"}}},"daily":{"type":"array","items":{"type":"object"}},"byPlatform":{"type":"array","items":{"type":"object"}},"topPaths":{"type":"array","items":{"type":"object"}},"disclosure":{"type":"object","properties":{"unit":{"type":"string"},"undercount":{"type":"string"},"identity":{"type":"string"}}}}}}}},"400":{"description":"Malformed id, dates not YYYY-MM-DD, reversed, or outside the retention window."},"401":{"description":"Unauthorized."},"403":{"description":"Key not scoped to this project."},"404":{"description":"Project not found."}}}},"/api/segments":{"get":{"tags":["Segments"],"summary":"List the project's segments","description":"Every segment of the active project, in switcher order, each with how many prompts it holds and which brands its roster is made of. A segment is a read-time lens, not a second measurement: answers keep being measured against every brand the project tracks, and a segment decides which prompts are in the cohort and which brands count when the numbers are read.","responses":{"200":{"description":"Segments and their count.","content":{"application/json":{"schema":{"type":"object","properties":{"segments":{"type":"array","items":{"$ref":"#/components/schemas/Segment"}},"count":{"type":"integer"}}}}}},"400":{"description":"No active project."},"401":{"description":"Unauthorized."}}},"post":{"tags":["Segments"],"summary":"Create a segment","description":"Needs editor access. In `own` roster mode the roster is seeded from the project's current competitors unless brandIds says otherwise, so the first screen after the switch equals the last one before it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":100},"kind":{"type":"string","enum":["standard","catalog"]},"rosterMode":{"type":"string","enum":["project","own"]},"brandIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}},"responses":{"201":{"description":"The created segment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Segment"}}}},"400":{"description":"invalid_name, invalid_kind or invalid_roster_mode."},"401":{"description":"Unauthorized."},"403":{"description":"A viewer may read segments and not write them."},"404":{"description":"brand_not_found."},"409":{"description":"duplicate_segment."}}}},"/api/segments/{id}":{"patch":{"tags":["Segments"],"summary":"Rename a segment","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":100}}}}}},"responses":{"200":{"description":"The renamed segment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Segment"}}}},"401":{"description":"Unauthorized."},"403":{"description":"A viewer may read segments and not write them."},"404":{"description":"segment_not_found."},"409":{"description":"duplicate_segment."}}},"delete":{"tags":["Segments"],"summary":"Delete a segment","description":"Refused while the segment holds prompts, archived ones included, and refused for the default segment: a project with no segment has no cohort for a prompt to join.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted."},"401":{"description":"Unauthorized."},"403":{"description":"A viewer may read segments and not write them."},"404":{"description":"segment_not_found."},"409":{"description":"segment_is_default, or segment_has_prompts with the count that blocks it."}}}},"/api/results":{"get":{"tags":["Results"],"summary":"List raw LLM query results","description":"Returns answer previews by default. With answerId, returns the full answer and preserves each stored brand mention while adding countsAsOccurrence; relevance-gated name-drops and structural table headers or repeats are false.","parameters":[{"name":"answerId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Return this project-scoped answer in full under answer. Other query parameters are ignored."},{"name":"sourceKey","in":"query","schema":{"type":"string"},"description":"Canonical source host/path. Defaults to answers citing this exact page, including a root homepage."},{"name":"sourceView","in":"query","schema":{"type":"string","enum":["urls","domains"],"default":"urls"},"description":"With sourceKey, urls matches an exact page; domains matches its Sources domain group, including a single subreddit."},{"name":"promptId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"llm","in":"query","schema":{"type":"string","enum":["chatgpt","perplexity","gemini","claude","google_aio","grok","deepseek"]}},{"name":"from","in":"query","schema":{"type":"string"},"description":"ISO timestamp or YYYY-MM-DD at UTC midnight."},{"name":"to","in":"query","schema":{"type":"string"},"description":"An inclusive exact ISO timestamp, or YYYY-MM-DD for the whole UTC day."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"$ref":"#/components/parameters/segment"},{"name":"topic","in":"query","description":"One or more prompt topics, comma separated, matched without regard to case; also spelled `category`. A prompt with no topic is `Uncategorized`.","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of answer previews, or one full answer when answerId is supplied.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["results","pagination"],"properties":{"results":{"type":"array","items":{"type":"object"}},"pagination":{"type":"object"}}},{"type":"object","required":["answer"],"properties":{"answer":{"type":"object","required":["id","responseText","brandsMentioned"],"properties":{"id":{"type":"string","format":"uuid"},"responseText":{"type":"string","nullable":true},"brandsMentioned":{"type":"array","nullable":true,"description":"Stored mention evidence. Object entries add countsAsOccurrence without removing raw fields.","items":{"oneOf":[{"type":"object","additionalProperties":true,"properties":{"countsAsOccurrence":{"type":"boolean"}}},{"type":"string","description":"Legacy raw entry, preserved unchanged."}]}}}}}}]}}}},"400":{"description":"Invalid sourceKey, sourceView or range, or no active project."},"401":{"description":"Unauthorized."},"404":{"description":"The requested answer or segment is outside this project."},"429":{"description":"Rate limit exceeded."}}}},"/api/sources":{"get":{"tags":["Sources"],"summary":"List retained source links","description":"Retained answer source links, grouped by page or domain and ordered by count, with the denominator every share is out of. Explicitly unused retrieved links are excluded; legacy links with unknown support remain visible and are counted separately from confirmed support. A redirect is counted under its destination, never under the redirector's host. Ownership is resolved from current project brands. A source link belongs to an answer, not to a mention. A date range requires both from and to.","parameters":[{"name":"view","in":"query","schema":{"type":"string","enum":["urls","domains"]},"description":"Defaults to domains with a range and urls without one."},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"from","in":"query","schema":{"type":"string"},"description":"ISO timestamp or YYYY-MM-DD at UTC midnight. Paired with to."},{"name":"to","in":"query","schema":{"type":"string"},"description":"An inclusive exact ISO timestamp, or YYYY-MM-DD for the whole UTC day. Paired with from."}],"responses":{"200":{"description":"Sources, the denominator, the page's metrics and pagination.","content":{"application/json":{"schema":{"type":"object","properties":{"view":{"type":"string","enum":["urls","domains"]},"scope":{"type":"string","enum":["all_cited_sources"]},"sources":{"type":"array","items":{"type":"object","description":"key, url, host, title, ownership, classification, citationCount, answerCount, promptCount, firstCitedAt, lastCitedAt, citationsByLlm and support (supported and unknown counts)."}},"denominator":{"type":"object","properties":{"totalAnswers":{"type":"integer"},"answersWithCitations":{"type":"integer"}}},"metrics":{"type":"object","description":"citedRate — the share of answers whose sources were observable that cite one of the project's own domains, as { value, counts: { eligible, observed, cited }, delta, series }; value is null when no answer was observable, never 0, and citedRate is null without a range or past the first page — then ownDomainCitations, ownSourcesCited, citations and uniqueSources, each { value, delta, series }."},"pagination":{"type":"object","description":"limit, offset, count, totalSources and totalCitations over the whole matching set."}}}}}},"400":{"description":"Invalid date range or no active project."},"401":{"description":"Unauthorized."},"429":{"description":"Rate limit exceeded."}}}},"/api/sources/outreach":{"get":{"operationId":"get_sources_outreach","tags":["Sources"],"summary":"Read source outreach","description":"Read one project-scoped source's saved contact, pitch draft and status. Requires key. No message is sent.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"projectId","in":"query","required":false,"schema":{"description":"Project UUID; defaults to the active project.","type":"string"}},{"name":"key","in":"query","required":true,"schema":{"description":"Source key from list_sources. Required.","type":"string","minLength":1,"maxLength":2048}}],"responses":{"200":{"description":"The saved outreach record, or null on a read with no saved record."},"400":{"description":"Missing key or invalid contact, source URL, pitch or status."},"401":{"description":"Unauthorized."},"403":{"description":"Project is read-only for a write."},"404":{"description":"Project outside the caller's organization."}}},"patch":{"operationId":"patch_sources_outreach","tags":["Sources"],"summary":"Save source outreach","description":"Save the supplied public contact, editable pitch and tracked status for one project-scoped source. Requires write authority and key. No message is sent.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"projectId","in":"query","required":false,"schema":{"description":"Project UUID; defaults to the active project.","type":"string"}},{"name":"key","in":"query","required":true,"schema":{"description":"Source key from list_sources. Required.","type":"string","minLength":1,"maxLength":2048}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contact":{"type":"string","maxLength":500,"examples":["editor@example.com"]},"pitch":{"type":"string","maxLength":10000},"status":{"type":"string","enum":["draft","contacted","replied","placed","declined"]},"sourceUrl":{"nullable":true,"type":"string","maxLength":4096}},"required":["contact","pitch","status"]}}}},"responses":{"200":{"description":"The saved outreach record, or null on a read with no saved record."},"400":{"description":"Missing key or invalid contact, source URL, pitch or status."},"401":{"description":"Unauthorized."},"403":{"description":"Project is read-only for a write."},"404":{"description":"Project outside the caller's organization."}}},"post":{"operationId":"post_sources_outreach","tags":["Sources"],"summary":"Discover public source contacts","description":"Read one exact cited page by default, or a cited site/community when view is domains, plus up to two linked same-origin contact pages for published email/contact links. Requires project write authority. Candidates retain their page evidence and need review. No address is guessed, saved or messaged.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"projectId","in":"query","required":false,"schema":{"description":"Project UUID; defaults to the active project.","type":"string"}},{"name":"key","in":"query","required":true,"schema":{"description":"Source key from list_sources. Required.","type":"string","minLength":1,"maxLength":2048}},{"name":"view","in":"query","required":false,"schema":{"default":"urls","type":"string","enum":["urls","domains"]}}],"responses":{"200":{"description":"{contacts, pages, complete, scope, sent:false}. Fetch failures are explicit page outcomes."},"400":{"description":"Missing key or invalid contact, source URL, pitch or status."},"401":{"description":"Unauthorized."},"403":{"description":"Project is read-only for a write."},"404":{"description":"Project outside the caller's organization."},"429":{"description":"Contact discovery rate limit reached."}}}},"/api/sources/detail":{"get":{"tags":["Sources"],"summary":"One cited page or domain","description":"Which prompts carried it, which platforms, and a bounded sample of the most recent answers that cited it. The sample is a window onto stored answers and says so (complete: false); it is not a citation history.","parameters":[{"name":"key","in":"query","required":true,"schema":{"type":"string"},"description":"A source key as /api/sources returns it. Specify view to distinguish an exact root homepage from its host group."},{"name":"view","in":"query","schema":{"type":"string","enum":["urls","domains"]},"description":"urls matches the exact page; domains matches the domain or subreddit group. Omitted retains legacy page-or-group matching."},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","default":5,"maximum":25}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Source detail.","content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"object","description":"Exact SQL from/to bounds (nullable), and toInclusive; apply < to when false."},"source":{"type":"object","description":"key, host, url, title, citationCount, answerCount, classification, ownership, citingPrompts, platforms, samples."}}}}}},"400":{"description":"Missing key, invalid view or range, or no active project."},"401":{"description":"Unauthorized."},"404":{"description":"Nothing in this project and period cites that source."},"429":{"description":"Rate limit exceeded."}}}},"/api/sources/changes":{"get":{"tags":["Sources"],"summary":"What arrived and what fell away","description":"The given range against the equal range immediately before it, plus the five domains gained and lost most, and citation persistence — how much the cited domain set moves day to day, which says nothing about difficulty. A period nothing was measured in, or two periods measured on materially different shares of their days (a measurement gap), is reported as insufficient_data rather than as departures.","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Comparison.","content":{"application/json":{"schema":{"type":"object","properties":{"periods":{"type":"object","description":"current and previous: exact SQL from/to bounds, toInclusive, days, collected (whether any answer was measured at all), measuredDays (UTC calendar days with at least one measured answer) and coverage (measuredDays / days, three decimals). Apply < to when toInclusive is false."},"disclosures":{"type":"array","items":{"type":"string"},"description":"One sentence per platform whose model changed between the periods. That platform's answers are left out of every figure in the body."},"diff":{"type":"object","description":"status compared with newUrls, droppedUrls, newDomains, droppedDomains, each { items, shown, total }; or insufficient_data with a reason: current_not_collected, previous_not_collected, unequal_periods, or unequal_coverage (the smaller coverage under 0.8 of the larger — a measurement gap, not a change in citations)."},"movers":{"type":"object","description":"{ status, gained, lost, weighting }: five domains each way, ranked as weighting says; insufficient_data with both sides empty when the previous period was not measured."},"citationPersistence":{"type":"object"}}}}}},"400":{"description":"Missing or invalid range, or no active project."},"401":{"description":"Unauthorized."},"429":{"description":"Rate limit exceeded."}}}},"/api/sources/competitor-only":{"get":{"tags":["Sources"],"summary":"Prompts citing a competitor and never you","description":"Prompts whose answers cite a tracked competitor's domain and never one of the project's own, ranked by competitor citations. Prompts citing neither side are left out.","parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Competitor-only prompts.","content":{"application/json":{"schema":{"type":"object","properties":{"trackedDomains":{"type":"object","properties":{"ownDomains":{"type":"array","items":{"type":"string"}},"competitorDomains":{"type":"array","items":{"type":"string"}}}},"library":{"type":"object","description":"indexed: whether the Content library index holds any page for this project. pages: the project's live pages (public and not removed), the count the Content library reports as counts.indexed.live — not the number of pages a prompt can be matched to."},"prompts":{"type":"array","items":{"type":"object","description":"promptId, promptText, competitorCitations, competitorDomains, and bestPage: the project's own indexed page closest to the prompt ({ itemId, url, title, overlap }, a lexical match on the words in overlap) or null."}},"pagination":{"type":"object"}}}}}},"400":{"description":"Invalid range or no active project."},"401":{"description":"Unauthorized."},"429":{"description":"Rate limit exceeded."}}}},"/api/alerts/episodes":{"get":{"tags":["Alerts"],"summary":"List alert episodes","description":"The alert shelf with a lifecycle: each episode has a shared status (new, acknowledged, resolved), a severity, the evidence it was raised on (period, threshold, observed, link, next action), who acknowledged or resolved it and when, and the caller's own read state. Ordered open first, worst severity first, newest last; counts are over the whole matching set.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string"},"description":"new, acknowledged, resolved — repeated or comma-separated."},{"name":"type","in":"query","schema":{"type":"string"},"description":"alert_type values, repeated or comma-separated."},{"name":"severity","in":"query","schema":{"type":"string"},"description":"info, warning, critical — repeated or comma-separated."},{"name":"platform","in":"query","schema":{"type":"string"}},{"name":"kind","in":"query","schema":{"type":"string"},"description":"What the signal is about: prompt, brand (a competitor), domain (a cited source), check (a data source), slice (the project or one platform), topic, product, traffic (AI-referred visits to the site), or title (an alert from before subjects were stored) — repeated or comma-separated."},{"name":"isRead","in":"query","schema":{"type":"boolean"}},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Episodes, counts and pagination.","content":{"application/json":{"schema":{"type":"object","properties":{"episodes":{"type":"array","items":{"type":"object","description":"id, alertType, severity, status, title, description, evidence, subject, dedupKey, platform, region, promptId, queryResultId, isRead, createdAt, acknowledgedAt, acknowledgedBy, resolvedAt, resolvedBy, evidenceHref, and guidance: what the alert is about (about), one line on why it matters (whyItMatters) and the one place to act on it (action)."}},"counts":{"type":"object","properties":{"total":{"type":"integer"},"byStatus":{"type":"object"},"bySeverity":{"type":"object"},"byKind":{"type":"object","description":"Episodes per subject kind under every filter except kind; they add up to total when no kind is chosen."},"unread":{"type":"integer"},"open":{"type":"integer"}}},"pagination":{"type":"object"}}}}}},"400":{"description":"No active project."},"401":{"description":"Unauthorized."},"429":{"description":"Rate limit exceeded."}}}},"/api/alerts/{id}":{"patch":{"tags":["Alerts"],"summary":"Acknowledge or resolve an episode","description":"Moves one episode along its lifecycle. Status is the signal — saying a problem is handled, or over — so it takes project write authority; read state is separate and every reader may set it. Resolved is terminal: a recurrence opens a new episode.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["acknowledged","resolved"]}}}}}},"responses":{"200":{"description":"The updated episode."},"400":{"description":"No active project or an unknown status."},"401":{"description":"Unauthorized."},"403":{"description":"The project is read-only for the caller."},"404":{"description":"Alert not found."},"409":{"description":"Already acknowledged, or already resolved."}}}},"/api/alerts/{id}/read":{"put":{"tags":["Alerts"],"summary":"Mark an alert as read","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated alert."},"400":{"description":"No active project."},"401":{"description":"Unauthorized."},"404":{"description":"Alert not found."}}}},"/api/reports":{"get":{"tags":["Reports"],"summary":"List reports","description":"Return all generated reports for the authenticated user.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Array of report metadata objects."},"401":{"description":"Unauthorized."}}},"post":{"operationId":"post_reports","tags":["Reports"],"summary":"Generate a report","description":"Generate a visibility, prompt or competitor report for a date range, stored so it can be downloaded again in any format. The filters are stored with it and applied again on every download.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["visibility","prompt","competitor"]},"format":{"type":"string","enum":["csv","json","pdf"]},"periodStart":{"type":"string","format":"date","description":"First day of the period (YYYY-MM-DD), at most 31 inclusive days before periodEnd."},"periodEnd":{"type":"string","format":"date","description":"Last day of the period (YYYY-MM-DD)."},"platform":{"description":"Platform slugs: chatgpt, claude, perplexity, gemini, google_aio, grok, deepseek. An unknown one is refused with 400 unknown_platform. Refused on a visibility report, which reads the daily rollup.","anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"region":{"description":"ISO 3166-1 alpha-2 codes. Refused on a visibility report.","anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"classes":{"description":"Query classes: branded, non_brand or unknown. `all` is the absence of the filter. Refused on a visibility report.","anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"segmentId":{"description":"A segment of this project; an unknown one is refused with 404 segment_not_found.","type":"string"},"topic":{"description":"Refused with 400 topic_export_unsupported: topic-filtered views cannot be exported yet.","anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["type","format","periodStart","periodEnd"],"additionalProperties":{}}}}},"responses":{"201":{"description":"Report metadata (generation complete). filters.disclosure carries the disclosure the generation produced — report, period, population, calculationVersion, generatedAt, notes — and every downloaded output carries the same one."},"400":{"description":"Validation error, an unknown platform (unknown_platform), a topic filter (topic_export_unsupported), or filters on a visibility report."},"401":{"description":"Unauthorized."},"403":{"description":"The project is read-only for the caller (project_read_only)."},"404":{"description":"segment_not_found."},"413":{"description":"A PDF too large to render."},"429":{"description":"Rate limit exceeded, or another report is generating."}}}},"/api/reports/{id}":{"get":{"tags":["Reports"],"summary":"Get report metadata","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Report metadata object."},"404":{"description":"Not found."}}}},"/api/reports/{id}/download":{"get":{"tags":["Reports"],"summary":"Download a report","description":"Download the generated report file. Use the optional format query parameter to override the stored format (e.g. download a CSV report as PDF).","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["csv","json","pdf"]},"description":"Override the report format. If omitted, uses the format the report was generated with."}],"responses":{"200":{"description":"Report file download.","content":{"text/csv":{},"application/json":{},"application/pdf":{}}},"404":{"description":"Not found."}}}},"/api/audit/list":{"get":{"tags":["Site audits"],"summary":"List audits","description":"Audits visible to the organization, newest first, at most 50. projectId narrows to one project.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Array of audit lifecycle rows: id, url, domain, status, currentStage, progress, overallScore, geoScore, geoSubScores, modelId, createdAt, completedAt, and ownSite — whether the run read one of the project's own sites (primary domain, a subdomain, or another domain the brand lists)."},"400":{"description":"Malformed projectId."},"401":{"description":"Unauthorized."}}}},"/api/audit/{id}":{"get":{"tags":["Site audits"],"summary":"Get an audit","description":"The lifecycle every poll renders and, once the audit has completed, the stage outputs, scores, findings, executive summary and narrative.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Audit row."},"400":{"description":"Malformed id."},"401":{"description":"Unauthorized."},"404":{"description":"Not found."}}}},"/api/badge/{projectId}":{"get":{"tags":["Badge"],"summary":"Render the mention-rate badge","description":"An explicitly published SVG badge, read through its revocable token, stating the project's mention rate over the last 30 rolled-up days — the rate, the counts it is a ratio of, and the window. One colour whatever the number: it states a rate and does not grade it. When the window spans more than one calculation version the newest version slice is shown with the days it covers, and note says so.","security":[],"parameters":[{"name":"projectId","in":"path","required":true,"description":"Legacy segment name: supply the current badge token from Project settings › Badge, never a project UUID.","schema":{"type":"string"}},{"name":"style","in":"query","schema":{"type":"string","enum":["flat","flat-square"],"default":"flat"}},{"name":"format","in":"query","schema":{"type":"string","enum":["svg","json"],"default":"svg"}}],"responses":{"200":{"description":"The public badge, or with format=json its reading. Responses are no-store; a published badge with no measurements reports no data.","content":{"image/svg+xml":{},"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","enum":["Mention rate"]},"value":{"type":"string","description":"What the badge prints, e.g. 75% · 18/24 · 30d."},"rate":{"type":"number","nullable":true},"numerator":{"type":"integer","nullable":true},"denominator":{"type":"integer","nullable":true},"interval":{"type":"array","nullable":true,"items":{"type":"number"}},"range":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"},"days":{"type":"integer"}}},"calculationVersion":{"type":"string"},"note":{"type":"string","nullable":true},"style":{"type":"string","enum":["flat","flat-square"]}}}}}},"404":{"description":"Unknown, private or revoked badge, including old UUID embeds. No-store."}}}},"/api/billing":{"get":{"tags":["Billing"],"summary":"Get billing and usage info","description":"Return account and per-project usage, monthly cost, subscription state, and credit balances. Each active prompt-region is one $3 monthly unit. The owning account is billed across all its projects; seats cost nothing. The one-time $6 signup credit is applied to Stripe invoices and grants no free prompt allocation. Reading this is an owner-or-administrator action for a signed-in person; an account-wide API key may read it, and a selected-project key may not.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Billing info.","content":{"application/json":{"schema":{"type":"object","properties":{"customerCreditCents":{"type":"integer","description":"Actual Stripe customer credit balance."},"pendingCreditCents":{"type":"integer","description":"Credit awaiting transfer to Stripe."},"activePromptCount":{"type":"integer"},"billableUnitCount":{"type":"integer","description":"Active prompt-region units."},"monthlyCostCents":{"type":"integer","description":"Allocation at the offered rate, before invoice credit."},"billingConfigurationMismatch":{"type":"boolean","description":"Stored Stripe item differs from the offered price; inspect subscription.unitAmountCents and currency."},"unitPriceCents":{"type":"integer","enum":[300]},"seatCostCents":{"type":"integer","enum":[0]},"queriesLast30Days":{"type":"integer"},"usageSince":{"type":"string","format":"date-time"},"projects":{"type":"array","items":{"type":"object"}},"subscription":{"type":"object","nullable":true},"modelsAvailable":{"type":"integer"},"stripeCustomerId":{"type":"string","nullable":true}}}}}},"401":{"description":"Unauthorized."}}}},"/api/billing/allocation-export":{"get":{"tags":["Billing"],"summary":"Export cost by segment","description":"The cost table as a CSV, one line per segment, which is what an agency bills a client from. Columns: Client, Project, Project slug, Segment, Active prompts, Prompt-region units, Monthly cost (USD), Queries (last 30 days). Every figure is a re-serialisation of the same allocation /api/billing reads, never a second calculation, so the segment lines sum to their project and the project lines to the account total. A project with one segment is one line; the query count has no segment grain and is blank on a segment line. Money, so this is an owner-or-administrator action for a signed-in person; an account-wide API key may read it, and a selected-project key may not — a company bill is not a project.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"The CSV, as an attachment.","content":{"text/csv":{}}},"401":{"description":"Unauthorized."},"403":{"description":"insufficient_role. The body carries a lock with reason role, and no file."},"404":{"description":"No billing account."},"429":{"description":"Rate limited."}}}},"/api/api-keys":{"get":{"tags":["API Keys"],"summary":"List API keys","description":"Return all API keys for the authenticated user (key hash is not returned, only prefix).","security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Array of API key metadata objects."},"401":{"description":"Unauthorized."}}},"post":{"tags":["API Keys"],"summary":"Create an API key","description":"Generate a new API key. The full key is returned ONCE in the response. Store it securely.","security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Human-readable label for the key."},"scope":{"type":"string","enum":["all","selected"],"default":"all","description":"Which projects the key reaches."},"accessMode":{"type":"string","enum":["read","write"],"default":"read","description":"What the key may do inside them. Independent of scope; neither widens the other. Any other value is a 400."},"expiresAt":{"type":"string","format":"date-time","nullable":true,"description":"Optional expiry, enforced before any handler runs. Null is an indefinite key."},"canCreateProjects":{"type":"boolean","default":false},"projectIds":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Required and non-empty with scope selected."}}}}}},"responses":{"201":{"description":"API key created. The `key` field contains the full key (shown only once), alongside scope, accessMode, canCreateProjects and expiresAt."},"400":{"description":"Validation error."},"401":{"description":"Unauthorized."}}}},"/api/integrations/{id}/conversion-event":{"get":{"tags":["Settings"],"summary":"List registered GA4 key events and the saved choice","description":"Reads the selected GA4 property's registered key events. The saved choice remains visible with registered false if it was removed in GA4. Limited to 20 requests per minute per user and integration.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Registered events and current selection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GaConversionEventSelection"}}}},"400":{"description":"Invalid integration id."},"401":{"description":"Unauthorized."},"404":{"description":"Active GA4 integration unavailable in this scope."},"409":{"description":"Select a GA4 property first."},"429":{"description":"Request limit reached."},"502":{"description":"GA4 key events could not be read."}}},"put":{"tags":["Settings"],"summary":"Choose the GA4 key event used by Traffic","description":"Requires company integration-management authority. Verifies the event against the selected property's current registered events before atomically saving it. Limited to 20 requests per minute per user and integration.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["conversionEvent"],"properties":{"conversionEvent":{"type":"string","pattern":"^[A-Za-z][A-Za-z0-9_]{0,39}$"}}}}}},"responses":{"200":{"description":"Saved selection.","content":{"application/json":{"schema":{"type":"object","required":["conversionEvent"],"properties":{"conversionEvent":{"type":"string"}}}}}},"400":{"description":"Invalid JSON or GA4 key-event name."},"401":{"description":"Unauthorized."},"403":{"description":"Integration-management authority required."},"404":{"description":"Active GA4 integration unavailable in this scope."},"409":{"description":"No property selected, event not registered, or property changed during validation."},"429":{"description":"Request limit reached."},"502":{"description":"GA4 key events could not be read."}}}},"/api/settings":{"get":{"tags":["Settings"],"summary":"Get tracking settings","description":"Return stored email notification preferences.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Settings object."},"401":{"description":"Unauthorized."}}},"post":{"tags":["Settings"],"summary":"Update tracking settings","description":"Partially update email notification preferences. Only provided fields are changed.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"emailAlerts":{"type":"boolean"},"emailFrequency":{"type":"string","enum":["daily","weekly"]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Updated settings."},"400":{"description":"Invalid or retired setting."},"401":{"description":"Unauthorized."}}}},"/api/content/performance":{"get":{"tags":["Content"],"summary":"Content setup and page evidence","description":"Project-scoped setup, equal 28-day page-performance windows, search-query suggestions and stored demand. Use view=pages for at most 200 page rows without queryPages; truncated indicates more inventory exists. Omit view for full evidence. Search, Analytics and AI visibility remain separate; missing data is not zero.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Successful result."},"400":{"description":"Unsupported performance view or no active project."},"401":{"description":"Unauthorized."},"403":{"description":"Scope or role refused."}},"parameters":[{"name":"view","in":"query","required":false,"description":"Use pages for the bounded Page evidence projection; omit view for full evidence including queryPages.","schema":{"type":"string","enum":["pages"]}},{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}}]}},"/api/content/demand":{"post":{"tags":["Content"],"summary":"Collect topic demand","description":"Project write access and an active or trialing Content subscription. Send 1–20 keywords and a supported country. Twenty lookups per organization per UTC day are included; duplicate daily requests replay. No model allowance is used.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Successful result."},"401":{"description":"Unauthorized."},"403":{"description":"Scope or role refused."}},"parameters":[{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["keywords","country"],"properties":{"keywords":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","maxLength":250}},"country":{"type":"string","minLength":2,"maxLength":2}}}}}}}},"/api/content/artifacts/{id}/preview":{"post":{"tags":["Content"],"summary":"Preview a destination write","description":"Publisher browser session. Send destinationId, slug, optional revisionId and activate. Returns the adapter payload and location without writing or buying a model call.","security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Successful result."},"401":{"description":"Unauthorized."},"403":{"description":"Scope or role refused."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["destinationId","slug"],"properties":{"destinationId":{"type":"string","format":"uuid"},"slug":{"type":"string"},"revisionId":{"type":"string","format":"uuid"},"activate":{"type":"boolean"}}}}}}}},"/api/content/artifacts/{id}/mark-published":{"post":{"tags":["Content"],"summary":"Mark a piece as published by hand","description":"Publisher browser session. Send url, the page's live address (http or https). Records the approved piece's head revision as published there, for a project with no connected site destination. Nothing reads the page back: the publication says the URL was declared. One record per revision; a second submit is a 409.","security":[{"cookieAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}}}}},"responses":{"201":{"description":"{ publicationId, state: \"published\", remoteUrl }."},"400":{"description":"No active project, a malformed id, or an invalid url."},"401":{"description":"Unauthorized."},"403":{"description":"No active organization, or this person cannot publish."},"404":{"description":"Piece not found."},"409":{"description":"Not approved, nothing written yet, the project publishes through a connected site, an expert sign-off is missing, or this revision is already marked published."},"429":{"description":"Rate limited."}}}},"/api/content/model-credentials":{"get":{"tags":["Content"],"summary":"Available customer model keys","description":"Project readers see organization provider labels and fingerprints, never secrets.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"Successful result."},"401":{"description":"Unauthorized."},"403":{"description":"Scope or role refused."}},"parameters":[{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}}]},"post":{"tags":["Content"],"summary":"Save a customer model key","description":"Owner or administrator browser session. Send provider (openai, anthropic, z_ai), label and apiKey. Encrypted at rest; replaces the active key for that provider. No shared-key fallback.","security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Successful result."},"401":{"description":"Unauthorized."},"403":{"description":"Scope or role refused."}},"parameters":[{"name":"projectId","in":"query","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["provider","label","apiKey"],"properties":{"provider":{"type":"string","enum":["openai","anthropic","z_ai"]},"label":{"type":"string"},"apiKey":{"type":"string","writeOnly":true}}}}}}}},"/api/content/model-credentials/{id}":{"delete":{"tags":["Content"],"summary":"Revoke a customer model key","description":"Owner or administrator browser session. Stops subsequent use without changing saved content.","security":[{"cookieAuth":[]}],"responses":{"200":{"description":"Successful result."},"401":{"description":"Unauthorized."},"403":{"description":"Scope or role refused."}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}]}},"/api/content/work":{"get":{"tags":["Content"],"summary":"The content production and review queue","description":"One page of the project's live artifacts, with the stage counts for the whole project beside them so a state filter does not change the chips. Each item carries its editorial state, head revision, brand, destination, latest deterministic check result, whether a live approval still authorizes publishing it, the run that produced the head revision and the latest publication attempt. Editorial state, run state and publication state are three separate fields and are never merged. Archived artifacts are in neither the rows nor the counts.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"state","in":"query","description":"One or more editorial states, comma-separated: idea, brief, draft, in_review, changes_requested, approved. An unrecognised name is a 400, not a silent drop.","schema":{"type":"string"}},{"name":"stage","in":"query","description":"The pipeline stage: brief, draft, review or published. Published is a landed publication rather than an editorial state, and also answers `published`: the pieces and the indexed pages no piece became, in one list by recency. Wins over state when both are sent.","schema":{"type":"string","enum":["brief","draft","review","published"]}},{"name":"source","in":"query","description":"With stage=published: signalkit (pieces SignalKit wrote) or existing (indexed pages no piece became). Omitted, both. An unrecognised value is a 400.","schema":{"type":"string","enum":["signalkit","existing"]}},{"name":"brandId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"maximum":100}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"{ items, counts: { total, byEditorialState, byStage, publishedEntries }, pagination, destinations }; each item.publication carries byHand, plus published: { entries, counts: { all, signalkit, existing, missing }, siteOnly } when stage=published."},"400":{"description":"No active project, or a malformed state, stage, source, brandId, limit or offset."},"401":{"description":"Unauthorized."},"403":{"description":"No active organization, or this credential cannot read the project's content."},"429":{"description":"Rate limited."}}}},"/api/content/library":{"get":{"tags":["Content"],"summary":"Indexed pages and generated artifacts","description":"Everything the project's destinations hold, in one list. An indexed entry carries the destination's own identity for the page and whether the last completed index still found it there — presence is what that index saw, never a live fetch. A generated entry carries its editorial state and the state of its latest publication attempt, which are two facts and stay apart. `truncated` says when either source had more rows than the limit could carry, because the merge is a slice over two clocks rather than a cursor.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"destinationId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"linked","in":"query","description":"Whether an entry has an artifact attached. A generated entry IS an artifact, so false narrows to indexed pages nobody is working on.","schema":{"type":"boolean"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"{ items, counts, truncated, pagination }. counts: total (distinct pages), indexed { total (drafts and removed included), present, removed, live (public and still there — the count every reader of \"your pages\" uses), linked, unlinked }, generated { total, merged }."},"400":{"description":"No active project, or a malformed destinationId, linked or limit."},"401":{"description":"Unauthorized."},"403":{"description":"No active organization, or this credential cannot read the project's content."},"429":{"description":"Rate limited."}}}},"/api/content/artifacts/{id}":{"get":{"tags":["Content"],"summary":"One artifact in full","description":"Every revision with its whole body, plus the checks, reviews, approvals and publication attempts recorded against each, the decision timeline, the publication gate with the reason it does not open, the run behind the head revision and the conversation it came out of. Whole bodies, because a diff between any two revisions is computed from this response rather than from a second round trip. Approval validity is recomputed on every read from the content hash, the destination's configuration version and the scoring policy — never stored as a flag. An artifact outside the caller's visible projects answers 404, not 403, so an id cannot be used to enumerate another customer's work.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{ artifact, brand, destination, currentRevisionId, revisions, approval, eligibility, blockers, run, decisions, conversation }."},"400":{"description":"No active project, or a malformed id."},"401":{"description":"Unauthorized."},"403":{"description":"No active organization."},"404":{"description":"Not found, or not readable by this credential."},"429":{"description":"Rate limited."}}}},"/api/content/automations":{"get":{"tags":["Content"],"summary":"Recipes and their recent runs","description":"Each recipe with the effective policy behind it and the stages, spend, retries and failure reason of every recent run. Runs carry usedUnits, heldUnits and the perRunLimit pinned by their revision. Growth runs expose semantic stage provider, model, cost and refusal detail. Auto-publish stays visible when a brand's mandatory expert review overrides it, and a run waiting on a person is reported as waiting rather than as failed.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"recipeId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"runLimit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"{ automations }."},"400":{"description":"No active project, or a malformed recipeId."},"401":{"description":"Unauthorized."},"403":{"description":"No active organization, or this credential cannot read the project's content."},"404":{"description":"A named recipe that is not this organization's."},"429":{"description":"Rate limited."}}},"post":{"tags":["Content"],"summary":"Create a disabled recipe","description":"Creates content generation or Growth analysis. Growth requires explicit managed or BYOK assignments for analysis, proposal, critique and repair, and accepts optional positive per-run and monthly allowance ceilings. It cannot carry publishing fields and starts disabled.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"201":{"description":"Recipe and first revision created disabled."},"400":{"description":"Invalid recipe, role assignment, schedule or ceiling."},"401":{"description":"Unauthorized."},"403":{"description":"Recipe-management authority required."},"429":{"description":"Rate limited."}}}},"/api/content/connections":{"get":{"tags":["Content"],"summary":"Publishing connections, grants and destinations","description":"Connections are organization-owned and one installation serves several projects, so the answer is account-wide although the request resolves a project for authority. Each grant names its project, and the affected-bindings report is given per grant and per connection, so the caller can tell what removing one project binding would break from what removing the account-wide connection would. No credential or encrypted field is in the response, by the shape of the view rather than by redaction.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"connectionId","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{ connections }, narrowed to the bindings this credential's project scope reaches."},"400":{"description":"No active project, or a malformed connectionId."},"401":{"description":"Unauthorized."},"403":{"description":"No active organization, or this credential cannot read the project's content."},"404":{"description":"A named connection that is not this organization's."},"429":{"description":"Rate limited."}}}},"/api/content/connections/{id}/astro-schema":{"get":{"tags":["Content"],"summary":"Suggest required fields from an Astro collection","description":"Owner or administrator session only. Reads a granted GitHub repository and branch without executing customer code. Suggestions cover one matching inline glob loader and a simple Zod object; dynamic, legacy and CloudCannon schemas remain manual.","security":[{"cookieAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"resourceId","in":"query","required":true,"schema":{"type":"string"}},{"name":"branch","in":"query","required":true,"schema":{"type":"string"}},{"name":"path","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ suggested: true, fields, source } or { suggested: false, reason }; no-store."},"400":{"description":"Missing or invalid collection mapping."},"401":{"description":"Unauthorized."},"403":{"description":"Only a session owner or administrator can inspect a connection."},"404":{"description":"Connection or project grant not found."},"429":{"description":"Vendor budget exceeded."}}}},"/api/content/knowledge":{"post":{"tags":["Content"],"summary":"Propose a brand learning","description":"Requires project write authority and a bound editorial brand. Creates a human-attributed proposal; a separate review must accept it before generation uses it.","security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","statement","rationale"],"properties":{"kind":{"type":"string","enum":["preference","fact","exception","covered_ground","rejected_topic"]},"statement":{"type":"string","maxLength":4000},"rationale":{"type":"string","maxLength":2000}}}}}},"responses":{"201":{"description":"Learning ID and proposed state."},"400":{"description":"Invalid learning or no active project."},"401":{"description":"Unauthorized."},"403":{"description":"Project write authority required."},"409":{"description":"Bind an editorial brand first."},"429":{"description":"Rate limited."}}},"get":{"tags":["Content"],"summary":"Editorial knowledge and guidance","description":"Approved documents, URLs, transcripts, product facts, examples and research packs with the provenance and access scope retrieval rechecks, plus guidance in all five of its states. A material's extracted body is never returned: it is private material under the retention policy and a list does not need it. sourceCounts.modelUsable counts nonempty, unredacted brand/project-readable materials. Keyed by the project's bound editorial brand; a project with no brand bound answers brand: null and empty lists rather than 404.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"kind","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer"}},{"name":"learningLimit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"The brand's knowledge sources, learnings and their counts."},"400":{"description":"No active project."},"401":{"description":"Unauthorized."},"403":{"description":"No active organization, or this credential cannot read the project's content."},"429":{"description":"Rate limited."}}}},"/api/content/billing":{"get":{"tags":["Content"],"summary":"The Content & Growth position","description":"The plan on offer, the subscription, the allowance balance, this period's included grant, retained top-ups and the trial. Every allowance figure is a sum over the immutable ledger rather than a stored counter. Separate from /api/billing, which is the monitoring subscription: the two are independent products on one account and may invoice on different dates. A session actor needs owner or administrator; an account-wide API key may poll its own spend, which is the split the two payment routes beside this one refuse outright.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"responses":{"200":{"description":"The plan, subscription, balance, grants and trial. plan.undecided names every commercial value not yet set on this deployment."},"401":{"description":"Unauthorized."},"403":{"description":"No active organization, or not an owner or administrator."},"404":{"description":"No billing account."},"429":{"description":"Rate limited."}}}},"/api/content/billing/checkout":{"post":{"tags":["Content"],"summary":"Start the Content & Growth subscription","description":"Creates a Stripe Checkout session. Browser session only, and owner or administrator on top of that: an API key is refused, because a key that could start a subscription would be a bearer token for the customer's card. Nothing here grants allowance — the grant is minted when Stripe reports the period paid, keyed on that period, so an abandoned session, a duplicate session and a redelivered event converge on one grant.","security":[{"cookieAuth":[]}],"responses":{"200":{"description":"The Checkout session URL and id."},"401":{"description":"Unauthorized."},"403":{"description":"An API key, no active organization, or not an owner or administrator."},"409":{"description":"already_subscribed."},"500":{"description":"checkout_misconfigured — the deployment has no return URL."},"502":{"description":"checkout_unavailable."},"503":{"description":"content_plan_unconfigured — this deployment has not been given the Stripe price it sells, and `undecided` names what is outstanding. Nothing is sold at an invented number."}}}},"/api/content/billing/top-up":{"post":{"tags":["Content"],"summary":"Buy top-up allowance packs","description":"Creates a card-only Stripe Checkout session for whole packs. Browser session only, owner or administrator, and an active or trialing Content subscription. A purchased top-up never lapses after cancellation, but spending it again needs a live subscription. The 1-100 bound on `packs` is an input safety bound, not a rate card.","security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["packs"],"properties":{"packs":{"type":"integer","minimum":1,"maximum":100}},"additionalProperties":false}}}},"responses":{"200":{"description":"The Checkout session URL and id."},"400":{"description":"invalid_pack_count."},"401":{"description":"Unauthorized."},"403":{"description":"An API key, no active organization, or not an owner or administrator."},"409":{"description":"subscription_required — an active or trialing Content subscription is needed before buying a pack."},"502":{"description":"checkout_unavailable."},"503":{"description":"content_plan_unconfigured — this deployment has not been given the Stripe price of a top-up pack."}}}},"/api/health":{"get":{"tags":["System"],"summary":"Health check","description":"Returns 200 if the service is running.","security":[],"responses":{"200":{"description":"OK."}}}},"/api/audit/run":{"post":{"operationId":"post_audit_run","tags":["Site audits"],"summary":"Start a site audit","description":"Queues a paid deep audit of one public URL from a project: a crawl, deterministic readiness checks, brand probes against the audit model and a synthesised report. The project's deep-audit allowance (2 audits per project every 30 days, shared with the dashboard and the MCP connector, and spent by a competitor's audit as by the project's own) is reserved atomically before anything is bought, keyed on project, host, depth and that host's last finished audit, so a resend while an audit is queued answers with it, and a request after it has finished audits afresh. Needs write access to the project.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Public http(s) URL."},"projectId":{"type":"string","minLength":1,"format":"uuid","description":"The project the audit belongs to."},"crawlDepth":{"default":10,"description":"How many pages to sample.","type":"integer","minimum":0,"maximum":25}},"required":["url","projectId"]}}}},"responses":{"200":{"description":"{ id, limit } for a queued audit, or the cached result of an audit already completed under this allowance (cached: true) with the limit summary."},"400":{"description":"Missing url or projectId, or a URL that is not public http(s)."},"401":{"description":"Unauthorized."},"402":{"description":"No active allocation or subscription."},"403":{"description":"The project is read-only for the caller."},"404":{"description":"Project not found."},"409":{"description":"The same audit is already queued."},"429":{"description":"Three audits per hour, or the 30-day allowance is spent."},"503":{"description":"Billing could not be verified."}}}},"/api/content/artifacts":{"post":{"operationId":"post_content_artifacts","tags":["Content"],"summary":"Create a content artifact","description":"Creates an editorial artifact and its first revision from text a person wrote, in the active project, as an idea, a brief or a draft (the default). Buys nothing. The brand is resolved against the organization and an archived one is refused. Naming a destination needs the publisher role, and the destination must be this project's, have passed a readiness test and accept the format. A refresh names the indexed page it replaces by refreshOfItemId or refreshUrl, not both, and a page has at most one live refresh. Needs write access to the project's content.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"brandId":{"type":"string","minLength":1,"format":"uuid","description":"The content brand the piece is written for."},"title":{"type":"string","minLength":1,"description":"Trimmed."},"body":{"description":"Markdown. Optional: an idea or a brief is often a title alone.","nullable":true,"type":"string"},"format":{"description":"Defaults to article.","nullable":true,"type":"string","minLength":1},"frontmatter":{"description":"Stored with the revision.","nullable":true,"type":"object","additionalProperties":{}},"state":{"description":"Defaults to draft.","type":"string","enum":["idea","brief","draft"]},"destinationId":{"description":"A publishing destination of this project.","nullable":true,"type":"string","minLength":1,"format":"uuid"},"refreshOfItemId":{"description":"The indexed page this piece refreshes.","nullable":true,"type":"string","minLength":1,"format":"uuid"},"refreshUrl":{"description":"The page this refreshes, by URL. One that matches no indexed page creates new work.","nullable":true,"type":"string"}},"required":["brandId","title"],"additionalProperties":false}}}},"responses":{"201":{"description":"{ id, projectId, brandId, format, title, state, destinationId, refreshOfItemId, revision }."},"400":{"description":"A field the contract refuses (its code names it), both refreshOfItemId and refreshUrl (ambiguous_refresh), or a destination that is not ready (destination_not_ready) or does not accept the format (format_not_accepted)."},"401":{"description":"Unauthorized."},"403":{"description":"The caller may not create content here, or name a destination without the publisher role."},"404":{"description":"brand_not_found, destination_not_found, refresh_item_not_found or refresh_page_not_found."},"409":{"description":"duplicate_topic, or refresh_already_open naming the artifact that holds the page."},"429":{"description":"Rate limit exceeded."}}}},"/api/content/artifacts/{id}/generate":{"post":{"operationId":"post_content_artifacts_id_generate","tags":["Content"],"summary":"Generate a revision of a content artifact","description":"Writes the artifact's next revision with the editorial loop (draft, then up to two critic retries per round), holding the content allowance before the first call and settling it after. The brand, project, attempt, budget and scoring policy come from the stored artifact and the server, never the body, so a resend before a new revision exists shares the first request's hold. Needs the right to revise the project's content and a Content & Growth allowance.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"topic":{"type":"string","minLength":1,"description":"What the revision is about."},"roles":{"type":"object","properties":{"draft":{"anyOf":[{"type":"object","properties":{"llm":{"type":"string","enum":["chatgpt","claude","perplexity","gemini","google_aio","grok","deepseek","google_ai_mode"],"description":"Managed platform. The app checks availability and generation support."}},"required":["llm"],"additionalProperties":false},{"type":"object","properties":{"byok":{"type":"object","properties":{"provider":{"type":"string","enum":["openai","anthropic","z_ai"]},"model":{"type":"string","description":"One of: openai gpt-4.1-mini; anthropic claude-sonnet-5-5, claude-sonnet-4-5; z_ai glm-4.6."}},"required":["provider","model"],"additionalProperties":false}},"required":["byok"],"additionalProperties":false}],"description":"Required: nothing chooses a drafting model for you."},"research":{"description":"Optional; absent skips the role.","nullable":true,"anyOf":[{"type":"object","properties":{"llm":{"type":"string","enum":["chatgpt","claude","perplexity","gemini","google_aio","grok","deepseek","google_ai_mode"],"description":"Managed platform. The app checks availability and generation support."}},"required":["llm"],"additionalProperties":false},{"type":"object","properties":{"byok":{"type":"object","properties":{"provider":{"type":"string","enum":["openai","anthropic","z_ai"]},"model":{"type":"string","description":"One of: openai gpt-4.1-mini; anthropic claude-sonnet-5-5, claude-sonnet-4-5; z_ai glm-4.6."}},"required":["provider","model"],"additionalProperties":false}},"required":["byok"],"additionalProperties":false}]},"factcheck":{"description":"Optional; absent skips the role.","nullable":true,"anyOf":[{"type":"object","properties":{"llm":{"type":"string","enum":["chatgpt","claude","perplexity","gemini","google_aio","grok","deepseek","google_ai_mode"],"description":"Managed platform. The app checks availability and generation support."}},"required":["llm"],"additionalProperties":false},{"type":"object","properties":{"byok":{"type":"object","properties":{"provider":{"type":"string","enum":["openai","anthropic","z_ai"]},"model":{"type":"string","description":"One of: openai gpt-4.1-mini; anthropic claude-sonnet-5-5, claude-sonnet-4-5; z_ai glm-4.6."}},"required":["provider","model"],"additionalProperties":false}},"required":["byok"],"additionalProperties":false}]},"critique":{"description":"Optional; absent skips the role.","nullable":true,"anyOf":[{"type":"object","properties":{"llm":{"type":"string","enum":["chatgpt","claude","perplexity","gemini","google_aio","grok","deepseek","google_ai_mode"],"description":"Managed platform. The app checks availability and generation support."}},"required":["llm"],"additionalProperties":false},{"type":"object","properties":{"byok":{"type":"object","properties":{"provider":{"type":"string","enum":["openai","anthropic","z_ai"]},"model":{"type":"string","description":"One of: openai gpt-4.1-mini; anthropic claude-sonnet-5-5, claude-sonnet-4-5; z_ai glm-4.6."}},"required":["provider","model"],"additionalProperties":false}},"required":["byok"],"additionalProperties":false}]},"repair":{"description":"Optional; absent skips the role.","nullable":true,"anyOf":[{"type":"object","properties":{"llm":{"type":"string","enum":["chatgpt","claude","perplexity","gemini","google_aio","grok","deepseek","google_ai_mode"],"description":"Managed platform. The app checks availability and generation support."}},"required":["llm"],"additionalProperties":false},{"type":"object","properties":{"byok":{"type":"object","properties":{"provider":{"type":"string","enum":["openai","anthropic","z_ai"]},"model":{"type":"string","description":"One of: openai gpt-4.1-mini; anthropic claude-sonnet-5-5, claude-sonnet-4-5; z_ai glm-4.6."}},"required":["provider","model"],"additionalProperties":false}},"required":["byok"],"additionalProperties":false}]}},"required":["draft"],"additionalProperties":false,"description":"Which model each pipeline role uses."},"format":{"description":"Defaults to the artifact's format.","nullable":true,"type":"string","minLength":1},"siteHost":{"description":"The site the piece will be published on, for internal links.","nullable":true,"type":"string"},"slug":{"description":"The path the piece will be published at.","nullable":true,"type":"string"}},"required":["topic","roles"],"additionalProperties":false}}}},"responses":{"200":{"description":"{ outcome, artifactId, revisionId, report, findings, attempt, allowance: { state, units, reservedUnits }, calls: [{ role, llm, model, status }] }."},"400":{"description":"A field the contract refuses (its code names it), role_incapable, override_forbidden or model_unavailable."},"401":{"description":"Unauthorized."},"402":{"description":"insufficient_allowance with the numbers, no_allowance, or a plan lock."},"403":{"description":"The caller may not revise this project's content."},"404":{"description":"Piece not found."},"409":{"description":"artifact_archived, brand_unresolved, topic_rejected, model_source_mismatch, model_credential_missing, or a hold already settled."},"429":{"description":"Ten generations per five minutes."}}}},"/api/content/plan":{"post":{"operationId":"post_content_plan","tags":["Content"],"summary":"Rebuild this week's content plan","description":"Without derived: re-reads the week's evidence and re-titles the plan with one model call, spending one content unit, or nothing when no evidence-backed titles exist to rewrite. With derived=1: builds this week's plan for free when the week has none, and refuses a week that already has one. Creates no briefs either way. Needs write access to the project.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"derived","in":"query","required":false,"schema":{"description":"1 builds this week's plan for free instead of re-titling it.","type":"string","enum":["1"]}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"responses":{"200":{"description":"{ plan, weekStart }, plus outcome: empty_plan and a message when nothing was re-titled."},"400":{"description":"No active project, a derived value other than 1, or a body."},"401":{"description":"Unauthorized."},"402":{"description":"The Content & Growth plan or allowance cannot cover a re-title, with a content lock."},"403":{"description":"The project is read-only for the caller."},"409":{"description":"generation_in_progress, or plan_exists for a derived build over this week's plan."},"429":{"description":"Four re-titles, or four derived builds, per project per hour."},"500":{"description":"The plan could not be assembled or titled; an unbilled failure releases the hold."}}}},"/api/prompts/suggest":{"post":{"operationId":"post_prompts_suggest","tags":["Prompts"],"summary":"Suggest prompts from brand and website context","description":"Generate prompt suggestions for the active project in one internal AI call, from the site and anything the body adds. Twenty runs per project every 30 days, shared by every member and API key; a run that returned nothing usable, or that the AI service could not start, is not counted. This operation depends on the configured internal AI service.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"count","in":"query","required":false,"schema":{"description":"How many to suggest. A larger number reads as 20, a malformed one as 10.","type":"integer","minimum":1,"maximum":20}},{"name":"url","in":"query","required":false,"schema":{"description":"A page to read as additional context. https:// is assumed when absent.","type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"idea":{"description":"What the customer wants to be found for, in their own words.","nullable":true,"type":"string","maxLength":500},"keywords":{"description":"Head terms, such as 'crm software'; questions are written around each. The first 20 are used, each cut to 80 characters.","type":"array","items":{"type":"string"}},"personas":{"description":"People to phrase the same need for. The first 6 are used, each cut to 120 characters.","type":"array","items":{"type":"string"}},"intents":{"description":"Stages of the decision to cover.","type":"array","items":{"type":"string","enum":["research","compare","buy"]}}}}}}},"responses":{"200":{"description":"Object containing suggestions with text, product/topic category, up to three tags, reason and optional aiSearchVolume (the AI-search volume of each question)."},"400":{"description":"No active project, an unknown intent (unknown_intent), an idea that is not text (invalid_idea) or longer than 500 characters (idea_too_long), or a url that is not a web address (invalid_url)."},"401":{"description":"Unauthorized."},"429":{"description":"Rate limit exceeded."},"503":{"description":"Suggestions are temporarily unavailable."}}}},"/api/projects/{id}/measurement-calibration":{"get":{"operationId":"get_projects_id_measurement_calibration","tags":["Dashboard"],"summary":"Read consumer-surface calibration","description":"Page separately through supplied consumer observations and recent API candidates with canPair, original-prompt confidence and the stored requested/served model, search, provider-route and calculation identity. The summary uses the latest 100 observations independently of either page. Historical answers with unknown wording stay listed but cannot be paired. Exact-name presence is measured consistently on both texts. The selected sample is not representative and never corrects dashboard rates.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"observationCursor","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":512}},{"name":"candidateCursor","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":512}},{"name":"observationLimit","in":"query","required":false,"schema":{"default":100,"type":"integer","minimum":1,"maximum":100}},{"name":"candidateLimit","in":"query","required":false,"schema":{"default":20,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Project's selected consumer observations, summary and recent API-answer candidates; or the recorded observation."},"400":{"description":"Invalid observation, future timestamp or mismatched prompt/region."},"401":{"description":"Unauthorized."},"403":{"description":"Project is read-only for a write."},"404":{"description":"Project or API answer outside the caller's scope."},"409":{"description":"No own-brand names are configured for comparison."},"429":{"description":"Rate limit exceeded."}}},"post":{"operationId":"post_projects_id_measurement_calibration","tags":["Dashboard"],"summary":"Record a consumer answer","description":"Pair a separately observed consumer answer with a stored API answer for the same prompt, platform and region. The caller attests this match. The observation snapshots the API answer's requested/served model, search, provider route and calculation identity where those facts were retained. Incomplete answers or observations more than 24 hours apart remain visible but excluded. No model call is bought.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"resultId":{"type":"string","minLength":1,"format":"uuid"},"promptText":{"type":"string","minLength":1,"maxLength":20000},"region":{"type":"string","minLength":1,"maxLength":10},"samePromptAndRegion":{"type":"boolean","enum":[true]},"consumerText":{"type":"string","minLength":1,"maxLength":100000},"observedAt":{"type":"string","format":"date-time"},"consumerModel":{"nullable":true,"type":"string","minLength":1,"maxLength":100},"sourceUrl":{"nullable":true,"type":"string","maxLength":4096},"searched":{"nullable":true,"type":"boolean"},"complete":{"type":"boolean"}},"required":["resultId","promptText","region","samePromptAndRegion","consumerText","observedAt","complete"]}}}},"responses":{"200":{"description":"Project's selected consumer observations, summary and recent API-answer candidates; or the recorded observation."},"400":{"description":"Invalid observation, future timestamp or mismatched prompt/region."},"401":{"description":"Unauthorized."},"403":{"description":"Project is read-only for a write."},"404":{"description":"Project or API answer outside the caller's scope."},"409":{"description":"No own-brand names are configured for comparison."},"429":{"description":"Rate limit exceeded."}}}},"/api/projects/{id}/products/suggestions":{"get":{"operationId":"get_projects_id_products_suggestions","tags":["Products"],"summary":"Read catalogue monitoring suggestions","description":"Read the cached review-only monitoring plan for one catalogue and the shared prompt-suggestion allowance. This call buys no model work and writes nothing.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"segmentId","in":"query","required":true,"schema":{"type":"string","minLength":1,"format":"uuid"}}],"responses":{"200":{"description":"The current cached review-only catalogue monitoring plan or generation status."},"400":{"description":"segmentId is missing or malformed."},"401":{"description":"Unauthorized."},"403":{"description":"The selected project is outside the caller's API-key scope."},"404":{"description":"Project or catalogue segment outside the caller's scope."},"409":{"description":"The segment is not a catalogue."},"429":{"description":"Rate limit reached."}}},"post":{"operationId":"post_projects_id_products_suggestions","tags":["Products"],"summary":"Suggest a catalogue monitoring plan","description":"Buy one prompt-suggestion run and propose catalogue-grounded segments, prompts and prompt applicability for review. Every selector is validated against the current catalogue. Nothing is applied, tracked or measured by this call.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"segmentId":{"type":"string","minLength":1,"format":"uuid"}},"required":["segmentId"]}}}},"responses":{"200":{"description":"A new or cached review-only catalogue monitoring plan."},"400":{"description":"segmentId is missing or malformed."},"401":{"description":"Unauthorized."},"402":{"description":"Monitoring is not active for this project."},"403":{"description":"The caller cannot write this project."},"404":{"description":"Project or catalogue segment outside the caller's scope."},"409":{"description":"The segment is not a catalogue, or generation is already pending."},"422":{"description":"The model returned no proposal grounded in current catalogue selectors."},"429":{"description":"Rate limit or prompt-suggestion allowance reached."},"503":{"description":"The internal AI service is unavailable."}}}},"/api/projects/{id}/measurement-profile":{"get":{"operationId":"get_projects_id_measurement_profile","tags":["Dashboard"],"summary":"Read the effective measurement profile","description":"Read the project's configured prompt/platform/region schedule and the deployment's requested model, search capability and cadence. Search capability means a tool is offered, not that every answer searched; observed use and whether the provider reported the served model are stored per answer. The profile states one scheduled measurement attempt per eligible cell, successful answers may be zero, weekly attempt counts are configured maxima, and there is no consumer-app parity or fabricated provider-cost forecast. This read runs no model and changes no schedule.","security":[{"cookieAuth":[]},{"apiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The current effective measurement profile."},"401":{"description":"Unauthorized."},"404":{"description":"Project outside the caller's scope."}}}}}}