{ "openapi": "3.0.0", "paths": { "/v1/health": { "get": { "description": "Check API availability and connectivity. Returns the current server timestamp.", "operationId": "PublicHealthController_getHealth_v1", "parameters": [], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfHealthResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/HealthResponse" } }, "required": [ "item" ] } } } } }, "summary": "Check API health", "tags": [ "Health" ] } }, "/v1/projects": { "get": { "description": "List all projects belonging to your organization. Returns project metadata and the most recent completed monitoring timestamp for each project.", "operationId": "PublicProjectsController_listProjects_v1", "parameters": [], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ListResponseOfProjectListItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/ProjectListItemResponse" } } }, "required": [ "items" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "List all projects", "tags": [ "Projects" ] } }, "/v1/projects/{projectId}": { "get": { "description": "Get detailed information about a specific project, including per-dimension monitoring freshness and AI monitoring prompts.", "operationId": "PublicProjectsController_getProject_v1", "parameters": [ { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfProjectDetailResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/ProjectDetailResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get project details", "tags": [ "Projects" ] } }, "/v1/projects/{projectId}/competitors": { "get": { "description": "List all competitors monitored within a project. Includes the project's own domain (marked with `isOwn: true`) and all added competitor domains.", "operationId": "PublicCompetitorsController_listCompetitors_v1", "parameters": [ { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ListResponseOfCompetitorListItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/CompetitorListItemResponse" } } }, "required": [ "items" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "List project competitors", "tags": [ "Competitors" ] } }, "/v1/projects/{projectId}/competitors/{competitorId}": { "get": { "description": "Get detailed information about a specific competitor within a project, including monitored page URLs (homepage and pricing).", "operationId": "PublicCompetitorsController_getCompetitor_v1", "parameters": [ { "name": "competitorId", "required": true, "in": "path", "description": "Competitor ID", "schema": { "example": "507f1f77bcf86cd799439012", "type": "string" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfCompetitorDetailResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/CompetitorDetailResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get competitor details", "tags": [ "Competitors" ] } }, "/v1/projects/{projectId}/tech-trust": { "get": { "description": "Get the latest tech & trust profile data for all competitors in a project. Returns security headers, trust signals, technology stack, robots.txt analysis, and DNS infrastructure. Trust signals are a fixed set of things we look for on a HOMEPAGE, in four categories — each category description names its exact membership and its denominator, so a count of 0 means none of THOSE signals was found and nothing more. Do not infer a signal a category does not list. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding. Two degradations are partial and worth distinguishing: shielded response headers null the security grade and score while the HTML-derived trust and tech counts stay real, and an unreadable robots.txt nulls only the AI-access fields. A project with no completed run for this dimension yet answers 404 `no_data_available` — not a missing project and not a failed measurement, just nothing measured here so far; the project itself is fine and `get_project` still describes it. Distinguish it from `project_not_found`, which means the id is wrong.", "operationId": "PublicTechTrustController_getTechTrustDashboard_v1", "parameters": [ { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfTechTrustDashboardResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/TechTrustDashboardResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get latest tech & trust data", "tags": [ "Tech & Trust Profile" ] } }, "/v1/projects/{projectId}/tech-trust/history": { "get": { "description": "Get paginated history of tech & trust monitoring runs for a project. Each entry includes run timing and summary statistics. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding. The two gap figures are null when there was no comparison to make — either side unmeasured, or no competitor to compare against — so a null gap is not a tie.", "operationId": "PublicTechTrustController_getTechTrustHistory_v1", "parameters": [ { "name": "page", "required": false, "in": "query", "description": "Page number (1-indexed)", "schema": { "minimum": 1, "default": 1, "example": 1, "type": "number" } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", "schema": { "minimum": 1, "maximum": 100, "default": 20, "example": 20, "type": "number" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "PaginatedResponseOfTechTrustHistoryItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/TechTrustHistoryItemResponse" } }, "pagination": { "$ref": "#/components/schemas/PaginationMeta" } }, "required": [ "items", "pagination" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get tech & trust run history", "tags": [ "Tech & Trust Profile" ] } }, "/v1/projects/{projectId}/tech-trust/history/{runId}": { "get": { "description": "Get full tech & trust data for a specific historical monitoring run, including per-competitor security headers, trust signals, technology stack, robots.txt, and DNS infrastructure. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding. A competitor whose robots.txt reads `exists: false` with NO availability marker genuinely publishes none, which allows all crawlers — a real finding, distinct from a null. A run that finished but produced no summary answers 404 `run_not_summarized`, which is a different fact from `run_not_found`: the run exists, it just has nothing to report. It used to answer 200 with an all-zero summary invented from the missing one.", "operationId": "PublicTechTrustController_getTechTrustRunDetail_v1", "parameters": [ { "name": "runId", "required": true, "in": "path", "description": "Run ID", "schema": { "example": "507f1f77bcf86cd799439012", "type": "string" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfTechTrustRunDetailResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/TechTrustRunDetailResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get tech & trust data for a specific run", "tags": [ "Tech & Trust Profile" ] } }, "/v1/projects/{projectId}/content": { "get": { "description": "Get the latest content intelligence data for all competitors in a project. Returns URL counts, content categorization, strategic URL analysis, and competitive gap assessment. Use this to understand the content landscape across competitors. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding. The four gap lists carry a distinction worth respecting: a null list means no comparison ran, while an EMPTY list means we compared and found none — reading the first as the second reports a verdict about a comparison that never happened. A project with no completed run for this dimension yet answers 404 `no_data_available` — not a missing project and not a failed measurement, just nothing measured here so far; the project itself is fine and `get_project` still describes it. Distinguish it from `project_not_found`, which means the id is wrong.", "operationId": "PublicContentController_getContentDashboard_v1", "parameters": [ { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfContentDashboardResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/ContentDashboardResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get latest content intelligence data", "tags": [ "Content Intelligence" ] } }, "/v1/projects/{projectId}/content/history": { "get": { "description": "Get paginated history of content intelligence monitoring runs for a project. Each entry includes run timing and summary statistics with gap analysis. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding.", "operationId": "PublicContentController_getContentHistory_v1", "parameters": [ { "name": "page", "required": false, "in": "query", "description": "Page number (1-indexed)", "schema": { "minimum": 1, "default": 1, "example": 1, "type": "number" } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", "schema": { "minimum": 1, "maximum": 100, "default": 20, "example": 20, "type": "number" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "PaginatedResponseOfContentHistoryItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/ContentHistoryItemResponse" } }, "pagination": { "$ref": "#/components/schemas/PaginationMeta" } }, "required": [ "items", "pagination" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get content intelligence run history", "tags": [ "Content Intelligence" ] } }, "/v1/projects/{projectId}/content/history/{runId}": { "get": { "description": "Get full content intelligence data for a specific historical monitoring run, including per-competitor URL counts, content categorization, and strategic URL metrics. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding. A run that finished but produced no summary answers 404 `run_not_summarized`, which is a different fact from `run_not_found`: the run exists, it just has nothing to report. It used to answer 200 with an all-zero summary invented from the missing one.", "operationId": "PublicContentController_getContentRunDetail_v1", "parameters": [ { "name": "runId", "required": true, "in": "path", "description": "Run ID", "schema": { "example": "507f1f77bcf86cd799439012", "type": "string" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfContentRunDetailResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/ContentRunDetailResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get content intelligence data for a specific run", "tags": [ "Content Intelligence" ] } }, "/v1/projects/{projectId}/content/changelog": { "get": { "description": "Get paginated changelog of detected URL changes across competitor sitemaps. Each item shows numeric counts per category plus up to 3 sample URLs per category by default — safe for any token budget. Supports filtering by `competitorId` and `category` query parameters. Pass `allUrlsPerCategory=true` for the full URL list per category (warning: high-activity competitors can produce large responses; combine with filters to scope, and check the `truncated` flag in the response — when true, the byte cap fired and items/URLs were trimmed).", "operationId": "PublicContentController_getContentChangelog_v1", "parameters": [ { "name": "page", "required": false, "in": "query", "description": "Page number (1-indexed)", "schema": { "minimum": 1, "default": 1, "example": 1, "type": "number" } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", "schema": { "minimum": 1, "maximum": 100, "default": 20, "example": 20, "type": "number" } }, { "name": "competitorId", "required": false, "in": "query", "description": "Filter by competitor ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } }, { "name": "category", "required": false, "in": "query", "description": "Filter by content category (blog, docs, tools, landing, caseStudies, comparison, integrations, changelog, webinars, legal, programmatic, other). Changelog rows carry the same category the content dashboard reports for the same URL, decided over the competitor's whole sitemap rather than over the changed URLs alone — so a page added into a templated catalog filters under `programmatic`, not `other`.", "schema": { "example": "blog", "type": "string", "enum": [ "blog", "docs", "tools", "landing", "caseStudies", "comparison", "integrations", "changelog", "webinars", "legal", "programmatic", "other" ] } }, { "name": "allUrlsPerCategory", "required": false, "in": "query", "description": "By default each item returns up to 3 sample URLs per category. Set true to return the full URL list per category. High-activity competitors can produce large responses — combine with `category` and `competitorId` filters to scope. Subject to an internal byte cap; check `truncated` in the response.", "schema": { "example": false, "type": "boolean" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "PaginatedResponseOfContentChangelogItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/ContentChangelogItemResponse" } }, "pagination": { "$ref": "#/components/schemas/PaginationMeta" }, "truncated": { "type": "boolean", "description": "True when the verbose-mode byte cap fired and items/URLs were trimmed to fit the response budget. Refine your query with category/competitorId filters when seen. Always false in default mode.", "example": false } }, "required": [ "items", "pagination", "truncated" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get content changelog", "tags": [ "Content Intelligence" ] } }, "/v1/projects/{projectId}/positioning": { "get": { "description": "Get the latest positioning data for all competitors in a project. Returns homepage messaging, value propositions, CTAs, target audience, and competitive messaging analysis. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding. For the messaging strings the distinction is sharp: a measured EMPTY STRING means we read the page and there is genuinely no call to action, which is a finding; a null means we never read it. A project with no completed run for this dimension yet answers 404 `no_data_available` — not a missing project and not a failed measurement, just nothing measured here so far; the project itself is fine and `get_project` still describes it. Distinguish it from `project_not_found`, which means the id is wrong.", "operationId": "PublicPositioningController_getPositioningDashboard_v1", "parameters": [ { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfPositioningDashboardResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/PositioningDashboardResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get latest positioning data", "tags": [ "Positioning" ] } }, "/v1/projects/{projectId}/positioning/history": { "get": { "description": "Get paginated history of positioning monitoring runs for a project. Each entry includes run timing and summary statistics with messaging gap analysis. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding.", "operationId": "PublicPositioningController_getPositioningHistory_v1", "parameters": [ { "name": "page", "required": false, "in": "query", "description": "Page number (1-indexed)", "schema": { "minimum": 1, "default": 1, "example": 1, "type": "number" } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", "schema": { "minimum": 1, "maximum": 100, "default": 20, "example": 20, "type": "number" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "PaginatedResponseOfPositioningHistoryItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/PositioningHistoryItemResponse" } }, "pagination": { "$ref": "#/components/schemas/PaginationMeta" } }, "required": [ "items", "pagination" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get positioning run history", "tags": [ "Positioning" ] } }, "/v1/projects/{projectId}/positioning/history/{runId}": { "get": { "description": "Get full positioning data for a specific historical monitoring run, including per-competitor homepage messaging, value propositions, CTAs, and audience data. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding. A run that finished but produced no summary answers 404 `run_not_summarized`, which is a different fact from `run_not_found`: the run exists, it just has nothing to report. It used to answer 200 with an all-zero summary invented from the missing one.", "operationId": "PublicPositioningController_getPositioningRunDetail_v1", "parameters": [ { "name": "runId", "required": true, "in": "path", "description": "Run ID", "schema": { "example": "507f1f77bcf86cd799439012", "type": "string" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfPositioningRunDetailResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/PositioningRunDetailResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get positioning data for a specific run", "tags": [ "Positioning" ] } }, "/v1/projects/{projectId}/pricing": { "get": { "description": "Get the latest pricing intelligence data for all competitors in a project. Returns structured pricing plans, free tier availability, billing options, enterprise pricing, promotions, and competitive gap analysis. Every tracked competitor appears — rows without pricing data carry a `pricingDataAvailable` reason (no pricing page found, their page didn't respond, or a temporary problem on our side) instead of being omitted; branch on it before quoting pricing facts. A pricing page is the single most likely thing to be missing, so this is the dimension where the null contract matters most: when your own pricing couldn't be analyzed, every metric on `summary.customer` is null and `pricingAnalysisAvailable` says why. `hasFreePlan: null` means we did not check, NOT that there is no free plan — a measured `false` is reported as `false` and is a real finding. The three gap flags do NOT share one null condition, so read each field's own description rather than gating all three on the same test: all three are null when your side was unmeasured or no competitor was measured, and `hasPriceGap` is null on two further conditions the other two don't carry — fewer than three comparable competitor prices, or your own price not being comparable to the market. Market figures (`marketAvgPrice`, `pricePositionPercent`) are null below that three-price threshold, which is 'not enough market to average', never zero. Comparable means fixed monthly amounts sharing one currency AND one licensed unit: a per-seat price is never averaged against a flat one, `marketPricingUnit` says which group was chosen, and every market figure must be quoted with it. A project with no completed run for this dimension yet answers 404 `no_data_available` — not a missing project and not a failed measurement, just nothing measured here so far; the project itself is fine and `get_project` still describes it. Distinguish it from `project_not_found`, which means the id is wrong.", "operationId": "PublicPricingController_getPricingDashboard_v1", "parameters": [ { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfPricingDashboardResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/PricingDashboardResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get latest pricing intelligence data", "tags": [ "Pricing Intelligence" ] } }, "/v1/projects/{projectId}/pricing/history": { "get": { "description": "Get paginated history of pricing intelligence monitoring runs for a project. Each entry includes run timing and summary statistics with pricing gap analysis. Fields we could not measure are null, with a sibling availability object giving the reason; never read a null as zero, false or absent, and note that a measured 0 / false / empty list is reported as itself and is a real finding.", "operationId": "PublicPricingController_getPricingHistory_v1", "parameters": [ { "name": "page", "required": false, "in": "query", "description": "Page number (1-indexed)", "schema": { "minimum": 1, "default": 1, "example": 1, "type": "number" } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", "schema": { "minimum": 1, "maximum": 100, "default": 20, "example": 20, "type": "number" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "PaginatedResponseOfPricingHistoryItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/PricingHistoryItemResponse" } }, "pagination": { "$ref": "#/components/schemas/PaginationMeta" } }, "required": [ "items", "pagination" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get pricing run history", "tags": [ "Pricing Intelligence" ] } }, "/v1/projects/{projectId}/pricing/history/{runId}": { "get": { "description": "Get full pricing intelligence data for a specific historical monitoring run, including per-competitor pricing plans, billing options, free tiers, and enterprise pricing. Competitors the run could not measure appear with a `pricingDataAvailable` reason instead of being omitted. Fields we could not measure are null; never read a null as zero, false or absent, and note that a measured 0 / false is reported as itself and is a real finding. A run that finished but produced no summary answers 404 `run_not_summarized`, which is a different fact from `run_not_found`: the run exists, it just has nothing to report. It used to answer 200 with an all-zero summary invented from the missing one.", "operationId": "PublicPricingController_getPricingRunDetail_v1", "parameters": [ { "name": "runId", "required": true, "in": "path", "description": "Run ID", "schema": { "example": "507f1f77bcf86cd799439012", "type": "string" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfPricingRunDetailResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/PricingRunDetailResponse" } }, "required": [ "item" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get pricing data for a specific run", "tags": [ "Pricing Intelligence" ] } }, "/v1/projects/{projectId}/ai-visibility": { "get": { "description": "Get the latest AI visibility data for a project. Returns AI Visibility Scores, mention rates, and per-provider breakdowns for all competitors found across ChatGPT, Claude, and Gemini. Use this to understand how AI models perceive and recommend your brand vs competitors. Each check asks every prompt in the project against all 3 AI providers. Rates divide by the answers that came back, not the queries sent, and the queries-sent figure is not returned. Checks published under the full-coverage gate got a usable answer to every query they asked; checks published before that gate stay published and can have been scored over fewer answers, and the response does not say which kind a check is — so describe a rate as a share of the answers counted, never as a share of every query asked. When the most recent check was abandoned as incomplete, `latestCheckDataAvailable` is present and everything else describes an earlier check. Set `includeAnswers=true` to also get what the models said on that check — every prompt sent and every brand each model named in rank order, with its stated reasoning — narrowed by `brand=`, `provider=` or `promptIndex=`. Roughly 12k tokens unfiltered against 2k with `brand=`; use `summary.totalEntries` to size it first. `brand=` keeps every answer and empties the ones that did not name that domain, so you see where they win and where they are invisible. No filter changes a number under `summary`. The prose returned is the model's wording about the brands it named, not CompetLab's assessment of them. For 'which prompt am I losing on', read `summary.customer.perPrompt` first — it is on the plain response and costs nothing. A project with no scored check yet answers 404 `no_data_available` — not a missing project and not a failed measurement, just nothing measured here so far; the project itself is fine and `get_project` still describes it. Distinguish it from `project_not_found`, which means the id is wrong.", "operationId": "PublicAiVisibilityController_getAiVisibilityDashboard_v1", "parameters": [ { "name": "includeAnswers", "required": false, "in": "query", "description": "Set true to include the models' raw answers — every prompt sent and every brand each model named, with its stated reasoning. Off by default because the block is large: roughly 12k tokens unfiltered, against roughly 2k with `brand=`. Read `summary.totalEntries` to size it first (about 200 tokens per entry). The prose it returns is the model's wording about the brands it named, not CompetLab's assessment.", "schema": { "default": false, "example": false, "type": "boolean" } }, { "name": "provider", "required": false, "in": "query", "description": "Return only this model's answers. Requires `includeAnswers=true`. Does not change any number under `summary`, and does not narrow `providerStatus`.", "schema": { "$ref": "#/components/schemas/AiProvider" } }, { "name": "brand", "required": false, "in": "query", "description": "Return only the entries for this domain, across every answer. Requires `includeAnswers=true`. Matches `brands[].domain`, case-insensitively; brand NAMES are the model's own wording and vary between answers, so they are never matched. EVERY answer is still returned — the ones that did not name this domain come back with an empty `brands`, because 'this model answered and did not name them' is a finding, not an absence of data. A query that produced no answer at all is in `unansweredQueries` instead and asserts nothing about anyone. Ranks are unaffected: an entry keeps the position it held in the full answer. This is the cheapest way to ask where a competitor wins and where they are invisible.", "schema": { "example": "competitor.com", "type": "string" } }, { "name": "promptIndex", "required": false, "in": "query", "description": "Return only the answers for this prompt, across every model. Requires `includeAnswers=true`. Zero-based, matching the per-prompt index used elsewhere in this dimension.", "schema": { "example": 0, "type": "number" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfAiVisibilityDashboardResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/AiVisibilityDashboardResponse" } }, "required": [ "item" ] } } } }, "400": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiValidationErrorEnvelope" } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get latest AI visibility data", "tags": [ "AI Visibility" ] } }, "/v1/projects/{projectId}/ai-visibility/history": { "get": { "description": "Get paginated history of AI visibility checks for a project. Each entry includes check timing and summary statistics with customer metrics, top competitor, and competitor rankings. Only scored checks are listed — under the full-coverage gate a cycle that came back short is never scored and does not appear here. Checks published before that gate remain listed and can have been scored over fewer answers than they asked queries; the queries-sent figure is not returned, so a listed check cannot be shown to be fully covered. Each entry carries `summary.totalEntries`, which is the size preview for that check's raw answers via the check-detail route. This page is subject to a response size cap: when `truncated` is true, whole entries were dropped from the end of `items` and `pagination.hasMore` does NOT account for them — lower `limit` to see the rest rather than paging forward, which would skip them.", "operationId": "PublicAiVisibilityController_getAiVisibilityHistory_v1", "parameters": [ { "name": "page", "required": false, "in": "query", "description": "Page number (1-indexed)", "schema": { "minimum": 1, "default": 1, "example": 1, "type": "number" } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", "schema": { "minimum": 1, "maximum": 100, "default": 20, "example": 20, "type": "number" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "PaginatedResponseOfAiVisibilityHistoryItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/AiVisibilityHistoryItemResponse" } }, "pagination": { "$ref": "#/components/schemas/PaginationMeta" }, "truncated": { "type": "boolean", "description": "True when the page exceeded the response size cap and whole entries were dropped from the end of `items`. `pagination.total` still reports the true number of checks, so a withheld entry is distinguishable from one that does not exist — but `pagination.hasMore` does NOT account for dropped rows, so paging forward on `hasMore` alone would skip them silently. Lower `limit` and re-request this page instead.", "example": false } }, "required": [ "items", "pagination", "truncated" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get AI visibility check history", "tags": [ "AI Visibility" ] } }, "/v1/projects/{projectId}/ai-visibility/history/{checkId}": { "get": { "description": "Get full AI visibility data for a specific historical check: per-competitor rankings across all three AI providers, with mention rates and AI Visibility Scores, under `summary`. Rates here divide by the answers that came back, not the queries sent, and the queries-sent figure is not on `summary` — describe a rate as a share of the answers counted, never as a share of every query asked. Set `includeAnswers=true` to also get what the models actually said: every prompt sent, and every brand each model named in rank order with its stated reasoning, plus `providerStatus` and (where the check recorded its ask) `answerCoverage`. That block is large — roughly 12k tokens unfiltered, against roughly 2k with `brand=`. Check `summary.totalEntries` to size it first (about 200 tokens per entry), and prefer `brand=` (one competitor across every answer) or `provider=` (one model) over fetching everything; `promptIndex=` narrows to a single prompt. `provider` and `promptIndex` narrow the answers array; `brand` does not — it reduces the `brands` list inside each answer, so every answer still comes back and the ones that did not name that domain arrive with an empty `brands`, which is a finding rather than an absence. No filter changes any number under `summary`: those are stored, computed over the whole check, and never recomputed for a filtered view. Ranks are stable under filtering. Queries that produced no usable answer are listed separately in `unansweredQueries` rather than appearing as answers naming nobody — those are different facts, and an empty `brands` under a `brand` filter is NOT that case. `summary.customer.perPrompt`, when present, breaks the customer's result down per prompt at no extra cost — use it before reaching for `includeAnswers`.", "operationId": "PublicAiVisibilityController_getAiVisibilityCheckDetail_v1", "parameters": [ { "name": "checkId", "required": true, "in": "path", "description": "Check ID", "schema": { "example": "507f1f77bcf86cd799439012", "type": "string" } }, { "name": "includeAnswers", "required": false, "in": "query", "description": "Set true to include the models' raw answers — every prompt sent and every brand each model named, with its stated reasoning. Off by default because the block is large: roughly 12k tokens unfiltered, against roughly 2k with `brand=`. Read `summary.totalEntries` to size it first (about 200 tokens per entry). The prose it returns is the model's wording about the brands it named, not CompetLab's assessment.", "schema": { "default": false, "example": false, "type": "boolean" } }, { "name": "provider", "required": false, "in": "query", "description": "Return only this model's answers. Requires `includeAnswers=true`. Does not change any number under `summary`, and does not narrow `providerStatus`.", "schema": { "$ref": "#/components/schemas/AiProvider" } }, { "name": "brand", "required": false, "in": "query", "description": "Return only the entries for this domain, across every answer. Requires `includeAnswers=true`. Matches `brands[].domain`, case-insensitively; brand NAMES are the model's own wording and vary between answers, so they are never matched. EVERY answer is still returned — the ones that did not name this domain come back with an empty `brands`, because 'this model answered and did not name them' is a finding, not an absence of data. A query that produced no answer at all is in `unansweredQueries` instead and asserts nothing about anyone. Ranks are unaffected: an entry keeps the position it held in the full answer. This is the cheapest way to ask where a competitor wins and where they are invisible.", "schema": { "example": "competitor.com", "type": "string" } }, { "name": "promptIndex", "required": false, "in": "query", "description": "Return only the answers for this prompt, across every model. Requires `includeAnswers=true`. Zero-based, matching the per-prompt index used elsewhere in this dimension.", "schema": { "example": 0, "type": "number" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfAiVisibilityCheckDetailResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/AiVisibilityCheckDetailResponse" } }, "required": [ "item" ] } } } }, "400": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiValidationErrorEnvelope" } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get AI visibility data for a specific check", "tags": [ "AI Visibility" ] } }, "/v1/projects/{projectId}/ai-visibility/trend": { "get": { "description": "Get AI visibility trend data over time for chart visualization. Returns time-series data points with customer and top competitor mention rates and AI Visibility Scores. Optionally filter by date range or specific AI provider — the provider filter applies to mention rates only; the AI Visibility Score is a single weighted composite across all providers, so customerAiScore/topCompetitorAiScore are null when a provider filter is applied. Every point comes from a scored check; under the full-coverage gate a cycle that did not get an answer to every query it asked has no point at all and is reported separately in `incompleteCycles`, so a gap in the series is distinguishable from a period when nothing was scheduled. Checks published before that gate stay plotted and can have been scored over fewer answers than they asked queries, and no point reports how many answers it counted. Returns up to 200 data points — use dateFrom/dateTo to narrow the range for longer histories.", "operationId": "PublicAiVisibilityController_getAiVisibilityTrend_v1", "parameters": [ { "name": "dateFrom", "required": false, "in": "query", "description": "Start date for trend data (ISO-8601)", "schema": { "example": "2026-01-01", "type": "string" } }, { "name": "dateTo", "required": false, "in": "query", "description": "End date for trend data (ISO-8601)", "schema": { "example": "2026-03-15", "type": "string" } }, { "name": "provider", "required": false, "in": "query", "description": "Filter by AI provider. Omit to get aggregated trend across all providers.", "schema": { "$ref": "#/components/schemas/AiProvider" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ListResponseOfAiVisibilityTrendDataPointResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/AiVisibilityTrendDataPointResponse" } }, "incompleteCycles": { "type": "array", "description": "Monitoring cycles inside the requested window that produced no data point, because we did not get a usable answer to every query they asked. Reported here rather than omitted so a gap in the line is distinguishable from a period when nothing was scheduled. Empty for a window with no such cycles, and empty for windows predating this field — an empty array never means \"we know there were no gaps\".", "items": { "$ref": "#/components/schemas/AiVisibilityIncompleteCycleResponse" } } }, "required": [ "items", "incompleteCycles" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get AI visibility trend data", "tags": [ "AI Visibility" ] } }, "/v1/projects/{projectId}/alerts": { "get": { "description": "Get paginated competitive intelligence alerts for a project. Returns alerts generated when monitoring detects meaningful changes in competitor activity — such as pricing restructures, content strategy shifts, or technology stack changes. Supports filtering by monitoring dimension (tech-trust, content, positioning, pricing, ai-visibility), severity level (critical, high, medium, info), and specific competitor. Alerts are sorted by creation date (newest first).", "operationId": "PublicAlertsController_listAlerts_v1", "parameters": [ { "name": "page", "required": false, "in": "query", "description": "Page number (1-indexed)", "schema": { "minimum": 1, "default": 1, "example": 1, "type": "number" } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", "schema": { "minimum": 1, "maximum": 100, "default": 20, "example": 20, "type": "number" } }, { "name": "dimension", "required": false, "in": "query", "description": "Filter by monitoring dimension (tech-trust, content, positioning, pricing, ai-visibility)", "schema": { "example": "pricing", "type": "string", "enum": [ "tech-trust", "content", "positioning", "pricing", "ai-visibility" ] } }, { "name": "severity", "required": false, "in": "query", "description": "Filter by alert severity (critical, high, medium, info)", "schema": { "example": "critical", "type": "string", "enum": [ "critical", "high", "medium", "info" ] } }, { "name": "competitorId", "required": false, "in": "query", "description": "Filter by competitor ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "PaginatedResponseOfAlertListItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/AlertListItemResponse" } }, "pagination": { "$ref": "#/components/schemas/PaginationMeta" } }, "required": [ "items", "pagination" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "List project alerts", "tags": [ "Alerts" ] } }, "/v1/projects/{projectId}/schedules": { "get": { "description": "Get monitoring schedules for all dimensions in a project. Returns the current configuration (enabled/disabled, interval) and timing (next run, last run) for each monitoring dimension. Use this to monitor scheduling health, detect stalled dimensions, or build alerting around missed runs.", "operationId": "PublicSchedulesController_listSchedules_v1", "parameters": [ { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ListResponseOfScheduleItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/ScheduleItemResponse" } } }, "required": [ "items" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "List monitoring schedules", "tags": [ "Schedules" ] } }, "/v1/projects/{projectId}/strategic-briefing": { "get": { "description": "Get the current state of this project's strategic briefing — the synthesized, as-of competitive read (what changed, what it means, what to do). This is the ANALYZED briefing; for the raw monitoring data use the per-dimension dashboards (e.g. `/pricing`), and for the monitored-competitor roster use `/competitors`.\n\n**This returns the LATEST run, whatever state it is in.** `meta.status` is that run's raw lifecycle status and is the field to branch on:\n\n- `done` — the run finished. `item`, `coverage` and `contains` are populated; this is the readable edition.\n- `running` — a briefing is being generated right now. `item` is null; `meta.progress` carries the step it is on and when it started. A typical run finishes in about 90 minutes and a healthy one can take up to ~2 hours — budget rather than busy-wait, and do not treat a run as late before then.\n- `failed` — the most recent attempt ended without producing an edition. `item` is null. A failed run does not resume the ~30-day cycle, so surface it rather than waiting it out.\n- `null` — this project has never had a briefing run at all. **This is the only value that means the project genuinely has nothing.**\n\n**When `meta.status` is not `done`, THIS RUN has no content — which says nothing about whether the project has a briefing.** A previously published edition is usually still readable: call `GET /v1/projects/{projectId}/strategic-briefing/history` to list the editions and `…/history/{runId}` to read one. **Never report that no briefing is available on the strength of a null `item` — check history first.**\n\nA missing or non-accessible project returns 404 `project_not_found`; the briefing lifecycle itself never 404s.\n\n**Sections — lean by default.** `sections` selects what to return and defaults to `[\"hub\"]` — the executive digest AND the navigation map. Each hub `diagnosis` row carries a `deepDive` pointer naming the dimension to read next; prefix it with `deep-` to form the section value (a pointer of `ai-visibility` → request `sections=deep-ai-visibility`). Valid sections: `hub`, `actions`, `competitors`, the thirteen `deep-` deep-dives, or `all` for the full document. Charts are off by default (`includeCharts=false` returns each chart's title + note only); pass `includeCharts=true` for the full data series.\n\n**Returned with any finished edition, regardless of `sections`:** `coverage` (methodology and data-quality caveats — read these before quoting any figure) and `contains` (the list of sections this edition actually holds, so you can pick what to fetch next instead of requesting slots blind). Both are null whenever `meta.status` is not `done`, which says nothing about whether earlier editions exist — check `…/history`.\n\n**Reading the numbers:** the AI Visibility Score is 0–100 (higher is better); an AI `rank` is ordinal (lower is better); action `impact` is 1–4 (higher is more critical); action effort is bucketed as `effortMinutes` (`<30` | `30-120` | `>120`, or `null` when the briefing gave that action no estimate — report it as unsized rather than as quick). A project's next briefing is scheduled approximately 30 days after its last **successful** one.", "operationId": "PublicBriefingController_getStrategicBriefing_v1", "parameters": [ { "name": "sections", "required": false, "in": "query", "description": "Which sections to return. Defaults to [\"hub\"] — the executive digest and navigation map. Pass specific sections to go deeper (e.g. a hub diagnosis pointer of `ai-visibility` maps to `deep-ai-visibility`), or `all` for the full document. Prefer deriving deep- values from the hub diagnosis pointers rather than requesting slots blind — the response's `contains` array lists exactly which sections exist for this edition, in this same vocabulary.", "schema": { "example": [ "hub" ], "type": "array", "items": { "type": "string", "enum": [ "hub", "actions", "competitors", "deep-ai-visibility", "deep-positioning", "deep-pricing", "deep-content", "deep-tech-trust", "deep-agent-readiness", "deep-ai-ecosystem", "deep-customer-voice", "deep-funding-capital", "deep-hiring-gtm", "deep-landscape", "deep-product-launches", "deep-reliability-status", "all" ] } } }, { "name": "includeCharts", "required": false, "in": "query", "description": "Include full chart series data. Defaults to false — each chart returns its title and note only, with no underlying numbers. Pass true for the full series.", "schema": { "default": false, "example": false, "type": "boolean" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BriefingEnvelopeResponse" } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get the current state of this project's strategic briefing", "tags": [ "Strategic Briefing" ] } }, "/v1/projects/{projectId}/strategic-briefing/history": { "get": { "description": "List this project's Strategic Briefing runs, newest first — one cheap metadata row each, never briefing content.\n\nEach row carries `runId` (pass it to `…/history/{runId}` to read that edition in full), `briefingDate`, `status`, the derived `editionNumber`, and `headline` — that edition's one-line verdict, so you can pick which one to open without fetching every one.\n\n**Runs that failed or are still generating are included**, with `editionNumber`, `briefingDate` and `headline` null. A gap between two editions is therefore explained rather than left unexplained.\n\nUse this to answer \"what did we say in April\", to trace how a read has changed over time, or — when `GET /strategic-briefing` reports a `running` or `failed` status — to find the last edition that is actually readable. For the current state use `GET /strategic-briefing`, not this.\n\nPaginated with `page` and `limit`; check `pagination.hasMore` to fetch further pages. Briefings are produced roughly monthly, so this list is typically short.", "operationId": "PublicBriefingController_getStrategicBriefingHistory_v1", "parameters": [ { "name": "page", "required": false, "in": "query", "description": "Page number (1-indexed)", "schema": { "minimum": 1, "default": 1, "example": 1, "type": "number" } }, { "name": "limit", "required": false, "in": "query", "description": "Number of items per page", "schema": { "minimum": 1, "maximum": 100, "default": 20, "example": 20, "type": "number" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "PaginatedResponseOfBriefingHistoryItemResponse", "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/BriefingHistoryItemResponse" } }, "pagination": { "$ref": "#/components/schemas/PaginationMeta" } }, "required": [ "items", "pagination" ] } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "List this project's briefing editions", "tags": [ "Strategic Briefing" ] } }, "/v1/projects/{projectId}/strategic-briefing/history/{runId}": { "get": { "description": "Get one strategic briefing edition by `runId`, in exactly the same shape as `GET /strategic-briefing`.\n\nTake the `runId` from `GET /strategic-briefing/history` (or from `meta.runId` on a current read). Accepts the same `sections` and `includeCharts` options, with the same `[\"hub\"]` default.\n\nA `runId` naming a run that failed or is still generating returns **200** with `meta.status` set accordingly and `item` null — that run genuinely produced no edition, which is an answer rather than an error. A malformed id returns 400 `invalid_run_id`; an id that does not belong to this project returns 404 `run_not_found`.\n\nUse this to read or quote a specific past edition. For the current state use `GET /strategic-briefing`.", "operationId": "PublicBriefingController_getStrategicBriefingEdition_v1", "parameters": [ { "name": "runId", "required": true, "in": "path", "description": "Briefing run ID, from `meta.runId` or a history row", "schema": { "example": "6a3950c06bda450a1372048e", "type": "string" } }, { "name": "sections", "required": false, "in": "query", "description": "Which sections to return. Defaults to [\"hub\"] — the executive digest and navigation map. Pass specific sections to go deeper (e.g. a hub diagnosis pointer of `ai-visibility` maps to `deep-ai-visibility`), or `all` for the full document. Prefer deriving deep- values from the hub diagnosis pointers rather than requesting slots blind — the response's `contains` array lists exactly which sections exist for this edition, in this same vocabulary.", "schema": { "example": [ "hub" ], "type": "array", "items": { "type": "string", "enum": [ "hub", "actions", "competitors", "deep-ai-visibility", "deep-positioning", "deep-pricing", "deep-content", "deep-tech-trust", "deep-agent-readiness", "deep-ai-ecosystem", "deep-customer-voice", "deep-funding-capital", "deep-hiring-gtm", "deep-landscape", "deep-product-launches", "deep-reliability-status", "all" ] } } }, { "name": "includeCharts", "required": false, "in": "query", "description": "Include full chart series data. Defaults to false — each chart returns its title and note only, with no underlying numbers. Pass true for the full series.", "schema": { "default": false, "example": false, "type": "boolean" } }, { "name": "projectId", "required": true, "in": "path", "description": "Project ID", "schema": { "example": "507f1f77bcf86cd799439011", "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BriefingEnvelopeResponse" } } } }, "400": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiValidationErrorEnvelope" } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "404": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiNotFoundErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Get one briefing edition by id", "tags": [ "Strategic Briefing" ] } }, "/v1/tools/tech-stack/scans": { "post": { "description": "Start an asynchronous tech-stack detection scan for a public domain. Detects the frontend frameworks, hosting and CDN infrastructure, CMS, analytics and growth tools, and customer engagement services in use. Returns sub-second with `item: { id, status: \"queued\", createdAt, expiresAt }` — poll `GET /v1/tools/tech-stack/scans/{scanId}` until `status` is `completed` or `failed`. Most scans complete in seconds; heavy-render sites can take up to ~90 seconds. Result and error payloads are persisted for 24h, after which the scan auto-deletes and GET returns 404. No project required. Suitable for ad-hoc competitive lookups, integration into onboarding flows, and pre-sales discovery — and the only supported shape for MCP clients (Cursor / Claude Desktop hardcode 60-second tool-call timeouts).", "operationId": "PublicTechStackToolController_createScan_v1", "parameters": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PTTechStackRequestDto" } } } }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfTechStackScanResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/TechStackScanResponse" } }, "required": [ "item" ], "example": { "item": { "id": "6638e9a1b2c3d4e5f6a7b8c9", "status": "queued", "createdAt": "2026-05-17T14:30:00.000Z", "expiresAt": "2026-05-18T14:30:00.000Z" } } } } } }, "400": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiValidationErrorEnvelope" } } } }, "401": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiUnauthorizedErrorEnvelope" } } } }, "429": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiRateLimitErrorEnvelope" } } } }, "502": { "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiBadGatewayErrorEnvelope" } } } } }, "security": [ { "CL-API-Key": [] } ], "summary": "Start an async tech-stack scan", "tags": [ "Tools" ] } }, "/v1/tools/tech-stack/scans/{scanId}": { "get": { "description": "Get the current state of an async tech-stack scan. Returns the scan's lifecycle (`queued` / `running` / `completed` / `failed`) along with `createdAt` and `expiresAt` always, plus `startedAt` once the runner picks it up and `completedAt` once it reaches a terminal state. On `completed` the full canonical scan response is in `item.result` (categorized technology hits with evidence). On `failed` the failure details are in `item.error` with a machine-readable `code` — currently `homepage_fetch_failed` (upstream homepage fetch threw) or `scan_failed` (any other error during scan execution). Scoped to the API key's organization — cross-tenant access, never-existed IDs, expired IDs (24h TTL), and malformed IDs all return 404 `scan_not_found` with an identical envelope to prevent existence-disclosure. Typically sub-second — safe to poll on a 2–5 second interval depending on your timeout budget.", "operationId": "PublicTechStackToolController_getScan_v1", "parameters": [ { "name": "scanId", "required": true, "in": "path", "schema": { "type": "string" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "title": "ItemResponseOfTechStackScanResponse", "type": "object", "properties": { "item": { "$ref": "#/components/schemas/TechStackScanResponse" } }, "required": [ "item" ], "example": { "item": { "id": "6638e9a1b2c3d4e5f6a7b8c9", "status": "completed", "createdAt": "2026-05-17T14:30:00.000Z", "expiresAt": "2026-05-18T14:30:00.000Z", "startedAt": "2026-05-17T14:30:00.412Z", "completedAt": "2026-05-17T14:30:45.218Z", "result": { "domain": "stripe.com", "finalUrl": "https://stripe.com/", "fetchedAt": "2026-05-17T14:30:45.000Z", "totalTechnologies": 3, "techStack": [ { "name": "Next.js", "evidenceCount": 2, "evidence": [ { "kind": "html", "source": "/_next/static/", "matched": "