{"openapi":"3.1.0","info":{"title":"Vaarta Competitive Signals API","version":"1.0.0","description":"Programmatic access to competitor signals, briefings, and on-demand snapshots. Authenticate with a Bearer API key (vt_live_...). All responses use the { data, meta } envelope; errors use { error: { code, message } }.","license":{"name":"Proprietary — Vaarta Terms of Service","url":"https://vaartahq.com/terms"},"contact":{"name":"Vaarta Support","url":"https://vaartahq.com/support","email":"support@vaartahq.com"}},"servers":[{"url":"https://api.vaartahq.com","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Signals","description":"Competitor signal events."},{"name":"Competitors","description":"Tracked competitors for your workspace."},{"name":"Briefings","description":"Generated competitive briefings."},{"name":"Snapshot","description":"On-demand competitor page snapshots."},{"name":"Usage","description":"API usage for the calling key."},{"name":"Digest","description":"The daily competitive-intelligence digest as a pull endpoint."},{"name":"Analysis","description":"Grounded Layer-2 implications derived from verified signals."},{"name":"Compare","description":"Positioning gap matrix: your product against a tracked competitor."},{"name":"Dossiers","description":"Standing per-competitor dossiers with versioning and revocable share links."},{"name":"MCP","description":"Model Context Protocol server. Connect an AI assistant to your signal record."},{"name":"Agents","description":"Self-serve access for AI agents: sign up with no human in the loop, look up a company, poll until it is onboarded, then read it through the signal, dossier and analysis endpoints."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"vt_live_*"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}}},"Meta":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"timestamp":{"type":"string","format":"date-time"},"pagination":{"type":"object","properties":{"limit":{"type":"integer"},"cursor":{"type":["string","null"]},"total":{"type":"integer"}}}}},"Signal":{"type":"object","description":"A competitor signal. Two kinds share this shape. A VERIFIED signal has passed the verification gate: it carries a model-assigned materiality_score and, where the gate recorded one, a verification_note. A LEDGER signal is reconstructed from the observation ledger (a newly published public repository, or a newly observed subdomain) and never passed the gate: verification_note is null, materiality_score is a fixed 0.50, and detail and why_it_matters are null. Ledger signals are returned only for workspaces that have enabled them in feed settings; they are off by default.","properties":{"id":{"type":"string"},"competitor":{"type":"string"},"signal_type":{"type":"string"},"severity":{"type":"string","enum":["high","medium","low"]},"title":{"type":"string"},"detail":{"type":["string","null"]},"materiality_score":{"type":["number","null"],"description":"Model-assigned materiality, 0-1. Always a JSON number. A fixed 0.50 on a ledger signal, which is not a judgement about the event."},"verification_note":{"type":["string","null"],"description":"What the gate checked. Null on a ledger signal, which was never gated - the one field that distinguishes the two kinds."},"source_url":{"type":["string","null"]},"detected_at":{"type":"string","format":"date-time","description":"When this workspace first OBSERVED the signal, not when the event occurred. A repository created in 2019 and made public today is stamped today. Ordering, the cursor and the since parameter all key on this field."}}},"SignalListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Signal"}},"meta":{"$ref":"#/components/schemas/Meta"}}},"BasisSpan":{"type":"object","description":"The computed span of the verified signals a claim rests on, so a reader can calibrate its confidence: a `likely` over one type in one month reads differently from a `likely` over four types across five months. Computed by the pipeline at write time, never by the model. Null on rows written before it existed.","properties":{"facts":{"type":"integer","description":"Cited signals (= basis_signal_ids.length)."},"types":{"type":"integer","description":"Distinct signal types among them."},"competitors":{"type":"integer","description":"Distinct competitors, industry items excluded."},"earliest":{"type":["string","null"],"format":"date","description":"Earliest dated fact; null when none is dated."},"latest":{"type":["string","null"],"format":"date"},"months":{"type":["integer","null"],"description":"Calendar months spanned, inclusive; null below two dated facts."},"undated":{"type":"integer","description":"Facts with no event date. A detection date is never substituted."},"version":{"type":"integer","description":"Shape version (1)."}}},"Analysis":{"type":"object","description":"One grounded implication. `basis_signal_ids` lists the verified signals the claim was derived from. An implication that cannot cite at least one real signal id is dropped during generation and never persisted, so every row served here is citable by construction.","properties":{"id":{"type":"string"},"subject_kind":{"type":"string","enum":["signal","competitor","dossier_section","portfolio","action"]},"subject_ref":{"type":"string","description":"What the claim is about: a signal id, a competitor name, or a dossier section key."},"claim":{"type":"string","description":"The implication text."},"basis_signal_ids":{"type":"array","items":{"type":"string"},"description":"Signal ids underwriting the claim. Never empty, so any claim can be traced back to the verified events it was drawn from rather than taken on trust. Ids generally resolve against `GET /v1/signals`; the exception is briefing-only signals, which that endpoint excludes by design. Executive-communication appearances below the notability floor are recorded briefing-only and can underwrite a `dossier_section` claim, so treat an id that returns nothing from `GET /v1/signals` as briefing-only rather than missing. The dossier surfaces resolve every basis signal regardless of routing."},"confidence":{"type":"string"},"model":{"type":"string","description":"Model that produced the claim."},"prompt_version":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"urgency":{"type":["string","null"],"enum":["low","medium","high",null],"description":"How soon the recommended move should be made. Set only on subject_kind 'action' rows; null on every other kind."},"basis_span":{"anyOf":[{"$ref":"#/components/schemas/BasisSpan"},{"type":"null"}],"description":"See BasisSpan."}}},"AnalysisListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Analysis"}},"meta":{"$ref":"#/components/schemas/Meta"}}},"DigestTopSignal":{"type":"object","properties":{"competitor":{"type":"string"},"signal_type":{"type":"string"},"severity":{"type":"string","enum":["high","medium","low"]},"title":{"type":"string"},"source_url":{"type":"string"}}},"DailyDigest":{"type":"object","description":"One tenant's daily digest for a date. On a date with no signals this is the empty-state object (headline \"No signals detected\", empty top_signals, zero counts) — never null.","properties":{"tenant_id":{"type":"string"},"period_date":{"type":"string","format":"date"},"headline":{"type":"string"},"summary":{"type":"string"},"top_signals":{"type":"array","items":{"$ref":"#/components/schemas/DigestTopSignal"}},"total_signals_count":{"type":"integer"},"older_signals_count":{"type":"integer","description":"Archival signals included in this digest (older than the staleness cutoff). A subset of total_signals_count."},"dashboard_url":{"type":"string"},"generated_at":{"type":"string","format":"date-time"}}},"DigestResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DailyDigest"},"meta":{"$ref":"#/components/schemas/Meta"}}},"ThesisItem":{"type":"object","description":"One grounded thesis claim within a layer of the weekly thesis.","properties":{"claim":{"type":"string","description":"The thesis text, with inline [Sn] citations."},"confidence":{"type":"string","enum":["consistent_with","likely","watch"]},"subject_ref":{"type":"string","description":"Period-week end for a portfolio item; competitor name for a competitor thesis."},"basis_signal_ids":{"type":"array","items":{"type":"string"},"description":"Signal ids underwriting the claim. Never empty (grounding is enforced at generation)."},"created_at":{"type":"string","format":"date-time"},"urgency":{"type":["string","null"],"enum":["low","medium","high",null],"description":"How soon the move should be made. Present on actions items only; absent on portfolio and competitor items."},"basis_span":{"anyOf":[{"$ref":"#/components/schemas/BasisSpan"},{"type":"null"}],"description":"See BasisSpan."}}},"Thesis":{"type":"object","description":"The composed weekly thesis, grouped by layer. Layers are empty when the weekly pass has not run this week.","properties":{"period_week":{"type":["string","null"],"description":"Latest portfolio period; null when none this week."},"portfolio":{"type":"array","items":{"$ref":"#/components/schemas/ThesisItem"},"description":"L3 cross-competitor patterns."},"competitors":{"type":"array","items":{"$ref":"#/components/schemas/ThesisItem"},"description":"L2 per-competitor theses."},"actions":{"type":"array","items":{"$ref":"#/components/schemas/ThesisItem"},"description":"L4 recommended moves — always empty unless the action layer is enabled for the account. Each item also carries an urgency (low|medium|high)."}}},"ThesisResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Thesis"},"meta":{"$ref":"#/components/schemas/Meta"}}},"GapDrivers":{"type":"object","description":"The four drivers a gap severity is composed from. size, freshness and breadth are computed by the pipeline from the two profiles and the week's other verdicts; weight is the model's judgement of how much the gap matters to this account's ICP and positioning. Severity is 0.40·size + 0.15·freshness + 0.15·breadth + 0.30·weight (renormalised when size is null), and 0 on parity or insufficient_data. Scores are comparable only within one drivers_version.","properties":{"size":{"type":["number","null"],"description":"0..1 magnitude of the gap; null where none is measurable (positioning)."},"size_detail":{"type":"string","description":"Computed, e.g. \"3 capabilities they list that we do not\"."},"freshness":{"type":"number","description":"0..1: 1 = evidence observed this run, 0 = 90 or more days old."},"freshness_as_of":{"type":"string","format":"date","description":"Date of the newest evidence on the gap side."},"breadth":{"type":"number","description":"0..1: share of the other competitors judged this week with the same verdict."},"breadth_detail":{"type":"string"},"weight":{"type":"number","description":"0..1, the model's: how much this gap matters given the account's ICP and positioning."}}},"GapDimension":{"type":"object","properties":{"dimension":{"type":"string","enum":["pricing","features","positioning","sentiment","gtm_hiring"]},"verdict":{"type":"string","enum":["self_ahead","self_behind","parity","diverged","insufficient_data"],"description":"Computed from the profiles for features and sentiment, and for pricing only as parity when both sides' entry prices and tier lists were fully compared and agree; the model's judgement otherwise. insufficient_data whenever either side lacks evidence."},"self_state":{"description":"The dimension's structured value on your side, carried from the profile."},"competitor_state":{"description":"The same on the competitor's side."},"severity":{"type":"number","description":"0..1, composed from drivers (see GapDrivers). 0 on parity and insufficient_data."},"severity_own":{"type":"number","description":"0..1, the same composition with breadth set to 0. Breadth moves with your competitor set, not with this competitor, so week-over-week movement and the history series read this number. Absent where drivers are absent."},"why_it_matters":{"type":"string"},"evidence":{"type":"object","properties":{"self":{"type":"array","items":{"type":"object","properties":{"source_url":{"type":"string"},"collected_at":{"type":"string"},"collector":{"type":"string"}}}},"competitor":{"type":"array","items":{"type":"object","properties":{"source_url":{"type":"string"},"collected_at":{"type":"string"},"collector":{"type":"string"}}}}}},"drivers":{"$ref":"#/components/schemas/GapDrivers","description":"Absent (not null) on matrices computed before drivers shipped (those carry no summary.drivers_version); a period_week read of an older week returns dimensions without it."}}},"GapMovement":{"type":"object","properties":{"dimension":{"type":"string"},"change":{"type":"string","enum":["gap_opened","gap_closed","gap_widened","gap_narrowed","unchanged","baseline"]},"delta_summary":{"type":"string"}}},"GapMatrix":{"type":"object","description":"The us-vs-them positioning gap for one competitor in one ISO week.","properties":{"competitor":{"type":"string"},"period_week":{"type":"string","description":"ISO week key, e.g. '2026-W37'."},"computed_at":{"type":"string","format":"date-time"},"dimensions":{"type":"array","items":{"$ref":"#/components/schemas/GapDimension"}},"summary":{"type":"object","properties":{"overall_posture":{"type":"string","enum":["self_ahead","self_behind","mixed","parity"]},"headline":{"type":"string"},"movement":{"type":"array","items":{"$ref":"#/components/schemas/GapMovement"},"description":"Versus the prior week. Every dimension reads baseline on the first week and whenever drivers_version changed."},"drivers_version":{"type":"integer","description":"The scoring version the severities were composed under. Absent on matrices computed before drivers shipped."},"gap_prompt_version":{"type":"string"}}},"history":{"type":"array","items":{"$ref":"#/components/schemas/GapHistoryWeek"},"description":"Present only when the request set weeks above 1. Oldest first, ending at the week of this matrix, within the history window of your plan and after any start-fresh date. Weeks from every drivers_version are included; compare severities only within one version."},"history_locked_weeks":{"type":"integer","description":"Present only when the request set weeks above 1. How many of the weeks asked for fall outside the history window of your plan and were withheld."}}},"GapHistoryDimension":{"type":"object","properties":{"dimension":{"type":"string","enum":["pricing","features","positioning","sentiment","gtm_hiring"]},"verdict":{"type":"string","enum":["self_ahead","self_behind","parity","diverged","insufficient_data"]},"severity":{"type":"number"},"severity_own":{"type":["number","null"],"description":"Severity with breadth set to 0; null on a week computed before drivers shipped."},"movement":{"type":["string","null"],"enum":["gap_opened","gap_closed","gap_widened","gap_narrowed","unchanged","baseline",null],"description":"Movement that week against its own prior week; null when none was recorded."}}},"GapHistoryWeek":{"type":"object","properties":{"period_week":{"type":"string","description":"ISO week key, e.g. '2026-W40'."},"computed_at":{"type":"string","format":"date-time"},"drivers_version":{"type":["integer","null"],"description":"null on a week computed before drivers shipped."},"dimensions":{"type":"array","items":{"$ref":"#/components/schemas/GapHistoryDimension"}}}},"GapMatrixResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/GapMatrix"},"meta":{"$ref":"#/components/schemas/Meta"}}}},"responses":{"Unauthorized":{"description":"Missing, invalid, or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"VerificationPending":{"description":"The key exists but is not active yet: an agent signup whose operator has not opened the verification link. Error code `verification_pending`. Distinct from 401, which means the key itself is wrong.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit exceeded. The per-minute limit is tier-differentiated (60 RPM free, 300 pro, 1000 team) and reported via the X-RateLimit-Limit/Remaining headers; retry after the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/v1/signals":{"get":{"operationId":"listSignals","tags":["Signals"],"summary":"List signals (paginated, keyset cursor)","description":"Free tier. Tenant-scoped. Industry signals excluded.","parameters":[{"name":"competitor","in":"query","schema":{"type":"string"}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"type","in":"query","schema":{"type":"string","enum":["pricing","feature","hiring","review","news"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of signals","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignalListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/signup":{"post":{"operationId":"agentSignup","tags":["Agents"],"summary":"Get an API key for an agent (no authentication)","description":"Creates an agent workspace and returns its API key once. The key stays inactive (`403 verification_pending`) until the operator opens the link emailed to `operator_email`, which works once and expires in 24 hours. Then call `POST /v1/lookups`. One live agent key per operator email. Signups that reach the database (accepted, refused as already registered, or refused as paused) are limited to 3 per client IP per hour, counted per /64 for IPv6; malformed requests do not count. Signups across all callers are also limited per hour; past that, signup answers `503 signups_paused` and creates nothing. If the email provider fails to send, the signup still succeeds with `verification_email: failed`, and `next` names the support address.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["operator_email"],"properties":{"operator_email":{"type":"string","format":"email","maxLength":254,"description":"The human who answers for this agent. The verification link goes here."},"agent_name":{"type":"string","maxLength":80,"description":"Shown to the operator. Letters, digits, spaces, `-` and `_`; anything else is removed. Defaults to `agent`."}}}}}},"responses":{"201":{"description":"Created. The raw key appears only in this response.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"api_key":{"type":"string","description":"vt_live_ followed by 64 hex characters. Shown once, never stored."},"key_prefix":{"type":"string"},"status":{"type":"string","enum":["pending_verification"]},"verification_email":{"type":"string","enum":["sent","failed"]},"sent_to":{"type":"string","description":"The operator email, masked."},"next":{"type":"string","description":"What to do next, in one sentence."}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"`invalid_parameter`: operator_email is not a valid address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent signup is not enabled on this server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`already_registered`: this operator email has a live agent key or an unexpired link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited`: too many signups from this IP in the last hour.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`signups_paused`: agent signups are paused, the cap on verified agent keys is reached, or this hour's signups across all callers are used. Nothing was created. Retry after an hour, or write to support@vaartahq.com.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/signup/verify":{"get":{"operationId":"agentSignupVerify","tags":["Agents"],"summary":"Activate an agent key (the emailed link)","description":"The link in the verification email. Opening it activates the key. Answers `text/plain` only. An unknown, used or expired token all answer the same 410; if the key still answers `verification_pending` after that, sign up again with the same operator email.","security":[],"parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Verified. The key is active.","content":{"text/plain":{"schema":{"type":"string"}}}},"404":{"description":"Agent signup is not enabled on this server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"The link expired or was already used.","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"The cap on verified agent keys is reached. The link was not used: open the same link again later, until it expires 24 hours after signup.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/v1/lookups":{"post":{"operationId":"createLookup","tags":["Agents"],"summary":"Look up a company (agent plan)","description":"Onboards a company into this agent workspace: every collector, the verification gate and the analysis passes run, then the company refreshes weekly. Returns 202 with a `lookup_id` to poll. A company the workspace already tracks returns 200 at once and uses no lookup. The agent plan allows 3 lookups per calendar month (UTC) and 3 tracked companies; every other plan adds companies with `POST /v1/competitors`. The first run gives mostly low-confidence theses; each weekly refresh adds evidence.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["homepage_url"],"properties":{"homepage_url":{"type":"string","format":"uri"},"name":{"type":"string","maxLength":80,"description":"Display name, 80 characters at most, no control characters. Defaults to the homepage host."},"urls":{"type":"object","additionalProperties":false,"properties":{"pricing":{"type":"string","format":"uri"},"changelog":{"type":"string","format":"uri"},"careers":{"type":"string","format":"uri"},"blog":{"type":"string","format":"uri"}}}}}}}},"responses":{"200":{"description":"Already tracked: the company, its most recent lookup, and no allowance used."},"202":{"description":"Accepted and queued.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"lookup_id":{"type":"string","format":"uuid"},"competitor":{"type":"string"},"status":{"type":"string","enum":["queued"]},"poll":{"type":"string","description":"GET this path until status is ok or failed."},"next":{"type":"string"},"depth":{"type":"string"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"`invalid_parameter` (a URL was rejected, an unknown key, or a name that is too long, has control characters, or starts and ends with `_`), or `use_competitors` (not an agent key).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"`verification_pending` (the key is not active yet), or `workspace_inactive` (this agent workspace was paused or closed; write to support@vaartahq.com).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`competitor_limit_reached` (remove a company with DELETE /v1/competitors/{id} first), or `name_taken` (another company on this key already has that name; pass a different one or leave it out).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`quota_exceeded` (this month's lookups are used; the error carries `period_resets_at`), or the per-key rate limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`lookups_paused_today`: the daily onboarding cap across all agents is reached; the error carries `period_resets_at`. Or `lookups_paused`: new lookups are switched off; tracked companies keep refreshing weekly.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/lookups/{id}":{"get":{"operationId":"getLookup","tags":["Agents"],"summary":"Poll a lookup","description":"Status is `queued`, `running`, `ok` or `failed`. Poll until it is `ok` or `failed`: both are final for this lookup. It is read from the runs that serve the lookup, so it survives a server restart: the first run that finished `ok`, else the latest one. A lookup the server could not run reads `failed`; the next weekly refresh can still onboard the company, and the status then reads `ok`. Once `ok`, `links` points at the signal, dossier and analysis endpoints for this company, and `consumed` records what the run used.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The lookup."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/VerificationPending"},"404":{"description":"No such lookup on this key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/competitors":{"get":{"operationId":"listCompetitors","tags":["Competitors"],"summary":"List tracked competitors (free)","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"operationId":"createCompetitor","tags":["Competitors"],"summary":"Add a competitor (pro+)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["competitor_name","competitor_urls"],"properties":{"competitor_name":{"type":"string"},"competitor_urls":{"type":"object","required":["homepage"],"properties":{"homepage":{"type":"string","format":"uri"},"pricing":{"type":"string","format":"uri"},"changelog":{"type":"string","format":"uri"},"careers":{"type":"string","format":"uri"},"blog":{"type":"string","format":"uri"}}}}}}}},"responses":{"201":{"description":"Created"},"400":{"description":"`invalid_parameter` (a missing or reserved name, a rejected URL, or no homepage), or `use_lookups` (an agent key, which adds companies with `POST /v1/lookups`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan limit reached or tier too low"},"409":{"description":"`name_taken`: another tracked competitor already has this name.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/competitors/{id}":{"delete":{"operationId":"deleteCompetitor","tags":["Competitors"],"summary":"Delete a competitor (ownership-checked)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found"}}}},"/v1/briefings":{"get":{"operationId":"listBriefings","tags":["Briefings"],"summary":"List briefings (free: metadata; pro+: includes summary)","parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","default":5,"maximum":10}},{"name":"cursor","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"A page of briefings"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/briefings/{id}":{"get":{"operationId":"getBriefing","tags":["Briefings"],"summary":"Full briefing JSON (pro+)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Tier too low"},"404":{"description":"Not found"}}}},"/v1/snapshot":{"post":{"operationId":"createSnapshot","tags":["Snapshot"],"summary":"Queue an on-demand snapshot (pro+, hard limit 3/hour)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}}}}},"responses":{"202":{"description":"Accepted — returns job_id"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Tier too low"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/snapshot/{job_id}":{"get":{"operationId":"getSnapshotStatus","tags":["Snapshot"],"summary":"Poll snapshot job status","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job status"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found"}}}},"/v1/usage":{"get":{"operationId":"getUsage","tags":["Usage"],"summary":"Current-month usage for the calling key (free)","description":"Calls by endpoint for the calendar month, the plan tier and the rate limit. On the agent plan the response also carries `lookups_used`, `lookups_limit`, `period_resets_at` (UTC) and `lookups`: the month's lookups with what each consumed: `serper_credits`; `gemini_flash_lite_calls` and its input and output tokens (the verification gate, collectors and follow-up queries); `gemini_synthesis_calls` and its tokens (the analysis passes, the briefing and compare); and `tracked_companies`, because a run refreshes every company the key tracks. These are the same rows the lookup allowance counts.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/digest":{"get":{"operationId":"getDigest","tags":["Digest"],"summary":"Daily digest for a date (pro+)","description":"The daily competitive-intelligence digest for a date (defaults to today): a headline, summary, and the top signals surfaced that day, reconstructed from verified signals. Always returns a digest — a date with no signals yields the empty-state object, never a 404 or a null body. Results are cached for one hour per (workspace, date); a small hourly cap bounds fresh generations. No range mode — one exact date per call. Tenant-scoped. Not on the agent plan: an agent key answers 403 `not_on_agent_plan` and reads `/v1/signals` instead.","parameters":[{"name":"date","in":"query","schema":{"type":"string","format":"date"},"description":"ISO date (YYYY-MM-DD). Defaults to today (UTC)."}],"responses":{"200":{"description":"The daily digest (empty-state object on a date with no signals).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DigestResponse"}}}},"400":{"description":"Malformed date"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Tier too low (pro+ required)"},"429":{"description":"Rate limited, or the hourly digest-generation cap was reached (error code digest_limit_exceeded). Retry-After is set."}}}},"/v1/compare":{"post":{"operationId":"getGapMatrix","tags":["Compare"],"summary":"Positioning Gap matrix for one competitor (pro+, read-only)","description":"Returns the latest computed us-vs-them gap matrix for a tracked competitor (or a specific ISO week). Read-only — never triggers collection; the gap is computed weekly. Tenant-scoped. Set weeks above 1 to add a weekly history of the verdict, severity and severity_own of each dimension, so whether a gap is closing is one call.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["competitor"],"properties":{"competitor":{"type":"string"},"period_week":{"type":"string","description":"ISO week key, e.g. '2026-W25'. Defaults to latest. An earlier week is returned only inside the history window of your plan and after any start-fresh date."},"weeks":{"type":"integer","minimum":1,"maximum":52,"default":1,"description":"Weeks of history to include, ending at the returned week. 1 (the default) returns the matrix alone."}}}}}},"responses":{"200":{"description":"The GapMatrix for that competitor/week","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GapMatrixResponse"}}}},"400":{"description":"Missing competitor, malformed period_week, or weeks outside 1 to 52"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Tier too low (pro+ required)"},"404":{"description":"No gap matrix computed yet for that competitor/week, a competitor you no longer track, or a named week other than the latest that falls outside the history window of your plan or before your start-fresh date"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/analysis":{"get":{"operationId":"listAnalyses","tags":["Analysis"],"summary":"Grounded Layer-2 implications (all plans)","description":"Returns implications produced by the weekly analysis pass over verified signals. Every claim carries `basis_signal_ids` naming the signals it was derived from. Grounding is enforced at generation time: a claim whose citations cannot be resolved to real signal ids is discarded rather than published, so this endpoint never serves an unsourced claim. Delivered as its own resource and never merged into the signal payload. Tenant-scoped.","parameters":[{"name":"competitor","in":"query","schema":{"type":"string"},"description":"Filter to one competitor, matching either the subject directly or the basis signal."},{"name":"subject_kind","in":"query","schema":{"type":"string","enum":["signal","competitor","dossier_section","portfolio","action"]}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisListResponse"}}}},"400":{"description":"Invalid subject_kind or since"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/thesis":{"get":{"operationId":"getThesis","tags":["Analysis"],"summary":"This week's composed thesis (all plans)","description":"The composed weekly thesis over verified signals, grouped into layers: cross-competitor portfolio patterns (L3) and a per-competitor thesis (L2) for each tracked competitor. Unlike /v1/analysis (a flat row list), this returns one coherent object. Every claim carries `basis_signal_ids`; a claim whose citations cannot be resolved is discarded at generation time. Layers are empty when the weekly analysis pass has not run this week. Tenant-scoped.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThesisResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}":{"get":{"operationId":"getDossier","tags":["Dossiers"],"summary":"Current dossier (pro+)","description":"The last finalized dossier for a competitor: its sections, version number and generation metadata.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}/live":{"get":{"operationId":"getDossierLive","tags":["Dossiers"],"summary":"Live dossier view (pro+)","description":"The dossier recomposed from signals collected since the last finalization, i.e. what the next version will contain.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}/versions":{"get":{"operationId":"listDossierVersions","tags":["Dossiers"],"summary":"List dossier versions (pro+)","description":"Every finalized version, newest first.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}/versions/{n}":{"get":{"operationId":"getDossierVersion","tags":["Dossiers"],"summary":"Fetch one dossier version (pro+)","description":"A specific finalized version by its number.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."},{"name":"n","in":"path","required":true,"schema":{"type":"integer"},"description":"Version number."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}/html":{"get":{"operationId":"getDossierHtml","tags":["Dossiers"],"summary":"Rendered dossier as HTML (pro+)","description":"The dossier rendered for display or sharing.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."},{"name":"theme","in":"query","schema":{"type":"string","enum":["dark","light"],"default":"dark"}},{"name":"version","in":"query","schema":{"type":"integer"},"description":"Render a specific version. Defaults to the latest."},{"name":"view","in":"query","schema":{"type":"string","enum":["live"]},"description":"Set to 'live' to render the uncommitted view instead of a finalized version."}],"responses":{"200":{"description":"OK","content":{"text/html":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}/pdf":{"get":{"operationId":"getDossierPdf","tags":["Dossiers"],"summary":"Rendered dossier as PDF (pro+)","description":"The dossier rendered to PDF. Not on the agent plan: an agent key answers 403 `not_on_agent_plan` and reads the JSON or `/html` view instead.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."},{"name":"version","in":"query","schema":{"type":"integer"},"description":"Defaults to the latest."}],"responses":{"200":{"description":"OK","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}/rebuild":{"post":{"operationId":"rebuildDossier","tags":["Dossiers"],"summary":"Rebuild the dossier now (pro+)","description":"Recomposes and finalizes a new version immediately rather than waiting for the scheduled run.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}/share":{"post":{"operationId":"createDossierShareToken","tags":["Dossiers"],"summary":"Create a share link (pro+)","description":"Issues a share token for the dossier. Tokens are stored hashed, so the raw token is returned once at creation and cannot be recovered afterwards — capture `token` from this response or reissue. Revoke via `DELETE /v1/dossier/{competitor}/share/{tokenHash}`.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."}],"requestBody":{"required":false,"description":"Optional. An empty body creates a live link with the default 30-day expiry.","content":{"application/json":{"schema":{"type":"object","properties":{"pinned_version":{"type":["integer","null"],"minimum":1,"description":"Pin the link to one finalized version. Omit or null for a live link that always shows the latest version. 404 if the version does not exist."},"expires_in_days":{"type":"integer","minimum":1,"maximum":365,"default":30,"description":"Lifetime of the link. Omit for the 30-day default. Outside 1–365, or non-integer, returns 400."}}}}}},"responses":{"200":{"description":"Created. `token` is the raw share token and is never returned again.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"token":{"type":"string","description":"Raw share token. Shown once."},"token_hash":{"type":"string","description":"Stable identifier for listing and revoking."},"pinned_version":{"type":["integer","null"]},"expires_at":{"type":"string","format":"date-time"}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"description":"Invalid pinned_version or expires_in_days"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}},"get":{"operationId":"listDossierShareTokens","tags":["Dossiers"],"summary":"List share links (pro+)","description":"Active share tokens, identified by hash. Raw tokens are never returned.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/v1/dossier/{competitor}/share/{tokenHash}":{"delete":{"operationId":"revokeDossierShareToken","tags":["Dossiers"],"summary":"Revoke a share link (pro+)","description":"Revokes a share token immediately.","parameters":[{"name":"competitor","in":"path","required":true,"schema":{"type":"string"},"description":"Competitor name as tracked in your workspace."},{"name":"tokenHash","in":"path","required":true,"schema":{"type":"string"},"description":"Hash of the token, as returned by the list endpoint."}],"responses":{"200":{"description":"Revoked"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Plan does not include dossiers (pro+ required)"},"404":{"description":"Not found. Generic by design so competitor names never leak across tenants."},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/mcp":{"post":{"operationId":"mcpJsonRpc","tags":["MCP"],"summary":"MCP endpoint (stateless Streamable HTTP)","description":"Model Context Protocol server. Point any MCP client (Claude Desktop, Claude Code, or your own agent) at this endpoint with a Vaarta API key and it can query your verified signal record directly.\n\nTransport is stateless Streamable HTTP: one JSON-RPC request per POST, one JSON response back, with no session to establish or tear down. Authentication, usage logging and the per-key rate limit reuse the same middleware stack as `/v1`, and the RPM budget is shared between the two surfaces.\n\nEvery request is bound server-side to the tenant that owns the API key, so tool arguments cannot be used to reach another workspace.\n\nPOST is the only method served. `GET /mcp` and `DELETE /mcp` both return 405 with JSON-RPC error -32000, because the stateless transport has no server-initiated stream to open and no session to terminate.\n\n**Tools exposed**\n- `get_signals`: deduplicated signals for a tracked competitor. Read-only. Most rows are gate-verified; a workspace that has opted into new-repository or new-subdomain rows also receives those, reconstructed from the observation ledger. No response field separates the two kinds; see the Signal schema.\n- `get_briefing`: the most recent weekly briefing. Read-only.\n- `get_thesis`: the current composed thesis: cross-competitor portfolio patterns plus a per-competitor thesis, each grounded in verified signals. Read-only.\n- `site_diff_run`: diff a competitor URL against its last snapshot. Pro and above; performs a live fetch.\n- `get_competitors`: list the competitors tracked in this workspace. Read-only.\n- `add_competitor`: start tracking a competitor (name + SSRF-validated URLs). Pro and above.\n- `remove_competitor`: stop tracking a competitor by id.\n- `get_compare`: the positioning-gap matrix for one competitor. Read-only; Pro and above.\n- `get_analysis`: grounded Layer-2 implications, filterable by competitor / subject_kind / since. Read-only.\n- `get_dossier`: a competitor dossier, finalized or live. Read-only; Pro and above.\n- `get_usage`: this key's current-month usage and rate limit. Read-only.\n- `get_digest`: the daily digest for a date (defaults to today). Read-only; Pro and above.\n- `lookup_company`: onboard a company into an agent workspace and refresh it weekly; returns a lookup_id. Agent plan only, with a monthly allowance.\n- `get_lookup`: poll a lookup: status, what it consumed, and where to read the results. Read-only.\n\nAn agent without a key gets one at `POST /v1/signup` (no authentication; the operator verifies by email).\n\n**Client configuration**\n\n```json\n{\n  \"mcpServers\": {\n    \"vaarta\": {\n      \"type\": \"http\",\n      \"url\": \"https://api.vaartahq.com/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer vt_live_your_key_here\" }\n    }\n  }\n}\n```","requestBody":{"required":true,"description":"A JSON-RPC 2.0 request, for example `tools/list` or `tools/call`.","content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"method":{"type":"string","examples":["tools/list","tools/call"]},"params":{"type":"object"},"id":{"type":["string","integer","null"]}}}}}},"responses":{"200":{"description":"JSON-RPC response.","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string"},"result":{"type":"object"},"error":{"type":"object"},"id":{"type":["string","integer","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/VerificationPending"},"429":{"$ref":"#/components/responses/RateLimited"}}}}}}