Skip to Content
DocsDevelopersSDKSDK Reference

SDK Reference

Conventions

A few things hold for every method on this page:

  • You destructure data. Each call returns { data, request, response }; data is the parsed body and is always present — the client throws on any non-2xx response, so a call that returns has succeeded.
  • Envelopes. A single resource returns { item }, a list returns { items }, and a paginated list returns { items, pagination } where pagination is { page, limit, total, totalPages, hasMore }. Paging is manual.
  • Errors throw. Any non-2xx response throws a CompetLabError with status, code, and message. It’s not a returned value; you catch it.
  • Types are exported. Every response type named below (ProjectDetailResponse, PaginationMeta, and the rest) is exported from @competlab/sdk for you to import.

Construct the client once and reuse it:

import CompetLab from '@competlab/sdk'; const cl = new CompetLab({ apiKey: process.env.COMPETLAB_API_KEY! });

health

Service liveness. The one method that needs no API key.

MethodParametersReturnsEndpoint
cl.health.check(){ item: HealthResponse }GET /v1/health

projects

Your projects — the top of every other call, since most methods take a projectId.

MethodParametersReturnsEndpoint
cl.projects.list(){ items: ProjectListItemResponse[] }GET /v1/projects
cl.projects.get(projectId)projectId: string{ item: ProjectDetailResponse }GET /v1/projects/{projectId}

competitors

The competitors tracked within a project.

MethodParametersReturnsEndpoint
cl.competitors.list(projectId)projectId: string{ items: CompetitorListItemResponse[] }GET /v1/projects/{projectId}/competitors
cl.competitors.get(projectId, competitorId)projectId: string, competitorId: string{ item: CompetitorDetailResponse }GET /v1/projects/{projectId}/competitors/{competitorId}

aiVisibility

How the AI engines mention your brand versus competitors — dashboard, history, one check’s detail, and a provider trend over time.

MethodParametersReturnsEndpoint
cl.aiVisibility.dashboard(projectId, query?)query?: { includeAnswers?, provider?, brand?, promptIndex? }{ item: AiVisibilityDashboardResponse }GET …/ai-visibility
cl.aiVisibility.history(projectId, query?)query?: { page?, limit? }{ items: AiVisibilityHistoryItemResponse[], pagination }GET …/ai-visibility/history
cl.aiVisibility.checkDetail(projectId, checkId, query?)query?: { includeAnswers?, provider?, brand?, promptIndex? }{ item: AiVisibilityCheckDetailResponse }GET …/ai-visibility/history/{checkId}
cl.aiVisibility.trend(projectId, query?)query?: { dateFrom?, dateTo?, provider? }{ items: AiVisibilityTrendDataPointResponse[] }GET …/ai-visibility/trend

The provider filter takes an AiProvider: 'openai' | 'claude' | 'gemini'. Note the wire value for ChatGPT is 'openai'cl.aiVisibility.trend(projectId, { provider: 'openai' }). The trend method returns a plain { items } list, not a paginated one.

Set includeAnswers: true on dashboard or checkDetail to also get the models’ raw answers — every prompt sent, and every brand each model named in rank order with its stated reasoning. It’s a large block (roughly 12k tokens unfiltered against ~2k with brand), so read summary.totalEntries to size it first, at about 200 tokens per entry. provider and promptIndex narrow the answers array; brand does not — it reduces the brands list inside each answer, so answers that didn’t name that domain still come back with an empty list, which is how you see where a competitor is invisible.

The prose in that block is unverified model output about the brands that model named, including third parties CompetLab doesn’t monitor. Attribute it to the named provider — it’s a record of what that model said, not CompetLab’s assessment.

positioning

Where you sit in the market narrative — dashboard, history, and one run’s detail.

MethodParametersReturnsEndpoint
cl.positioning.dashboard(projectId)projectId: string{ item: PositioningDashboardResponse }GET …/positioning
cl.positioning.history(projectId, query?)query?: { page?, limit? }{ items: PositioningHistoryItemResponse[], pagination }GET …/positioning/history
cl.positioning.runDetail(projectId, runId)projectId: string, runId: string{ item: PositioningRunDetailResponse }GET …/positioning/history/{runId}

pricing

Competitor pricing and plan changes — dashboard, history, and one run’s detail.

MethodParametersReturnsEndpoint
cl.pricing.dashboard(projectId)projectId: string{ item: PricingDashboardResponse }GET …/pricing
cl.pricing.history(projectId, query?)query?: { page?, limit? }{ items: PricingHistoryItemResponse[], pagination }GET …/pricing/history
cl.pricing.runDetail(projectId, runId)projectId: string, runId: string{ item: PricingRunDetailResponse }GET …/pricing/history/{runId}

content

Competitor content and messaging changes — dashboard, history, run detail, and a changelog of what changed.

MethodParametersReturnsEndpoint
cl.content.dashboard(projectId)projectId: string{ item: ContentDashboardResponse }GET …/content
cl.content.history(projectId, query?)query?: { page?, limit? }{ items: ContentHistoryItemResponse[], pagination }GET …/content/history
cl.content.runDetail(projectId, runId)projectId: string, runId: string{ item: ContentRunDetailResponse }GET …/content/history/{runId}
cl.content.changelog(projectId, query?)query?: { page?, limit?, competitorId?, category?, allUrlsPerCategory? }{ items: ContentChangelogItemResponse[], pagination, truncated }GET …/content/changelog

The changelog adds a truncated boolean to the envelope, set when the result was capped.

category filters by the same twelve the content dashboard counts — blog, docs, tools, landing, caseStudies, comparison, integrations, changelog, webinars, legal, programmatic, other. Since 3.2.0 it is a union of those twelve rather than a plain string, so a misspelling is a compile error — and TypeScript names the correction — instead of a 400 you discover at runtime.

To narrow a value that arrives as a plain string — a CLI argument, a query-string value — import the type rather than retyping the twelve literals, which go stale the moment a thirteenth category ships:

import { type ContentCategory } from '@competlab/sdk'

ContentCategory is exported from 3.3.0. Before that the union was declared inline and had to be reached through the method signature — NonNullable<NonNullable<Parameters<typeof cl.content.changelog>[1]>['category']> — which still compiles and resolves to the same union if you have it in your codebase.

Filtering by programmatic works: a changelog row carries the same category the content dashboard reports for that 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 rather than other.

techTrust

Tech stack and trust signals — dashboard, history, and one run’s detail.

MethodParametersReturnsEndpoint
cl.techTrust.dashboard(projectId)projectId: string{ item: TechTrustDashboardResponse }GET …/tech-trust
cl.techTrust.history(projectId, query?)query?: { page?, limit? }{ items: TechTrustHistoryItemResponse[], pagination }GET …/tech-trust/history
cl.techTrust.runDetail(projectId, runId)projectId: string, runId: string{ item: TechTrustRunDetailResponse }GET …/tech-trust/history/{runId}

Each competitor carries an optional aiAccess object — per-assistant reach across six named assistants, per-operator training access across nine, and the crawlers, directive and line number behind each verdict. 4.0.0 deleted allowsAiAccess, blockedAiBotsCount and aiBotsBlocked with no replacement boolean, so reading any of them is now a compile error rather than a wrong answer. Check aiAccess.measurement.status before the verdict arrays: on could_not_measure they are absent rather than empty, and ?? [] turns that into a false claim. See the omitted-key rule.

aiAccess verdicts live on the check, not the run summary — so dashboard and runDetail carry them and history does not.

alerts

Notable changes surfaced across the dimensions, filterable.

MethodParametersReturnsEndpoint
cl.alerts.list(projectId, query?)query?: { page?, limit?, dimension?, severity?, competitorId? }{ items: AlertListItemResponse[], pagination }GET …/alerts

dimension is one of tech-trust, content, positioning, pricing, ai-visibility; severity is one of critical, high, medium, info.

schedules

The monitoring cadence configured for a project.

MethodParametersReturnsEndpoint
cl.schedules.list(projectId)projectId: string{ items: ScheduleItemResponse[] }GET …/schedules

strategicBriefing

The synthesized monthly read across every dimension. Its envelope is the one exception to the { item } pattern.

MethodParametersReturnsEndpoint
cl.strategicBriefing.get(projectId, query?)query?: { sections?, includeCharts? }{ item, meta, coverage, contains }GET …/strategic-briefing
cl.strategicBriefing.history(projectId, query?)query?: { page?, limit? }{ items, pagination }GET …/strategic-briefing/history
cl.strategicBriefing.edition(projectId, runId, query?)query?: { sections?, includeCharts? }{ item, meta, coverage, contains }GET …/strategic-briefing/history/{runId}

Check meta.status'running' | 'done' | 'failed' | null — before reading item, which is null unless the latest run finished. Pass sections to fetch specific parts (e.g. ['deep-ai-visibility', 'actions']) and includeCharts: true for chart data.

const { data } = await cl.strategicBriefing.get(projectId, { sections: ['deep-ai-visibility', 'actions'], includeCharts: true, }); if (data.meta.status === 'done') { console.log(data.item); }

get() returns the latest run in whatever state it is in. Only meta.status === null means the project genuinely has no briefing — on running or failed, list past editions and read the newest finished one instead of reporting that none exists:

const { data } = await cl.strategicBriefing.get(projectId); if (!data.item && data.meta.status !== null) { const { data: past } = await cl.strategicBriefing.history(projectId); const newest = past.items.find((row) => row.status === 'done'); if (newest) { const { data: edition } = await cl.strategicBriefing.edition(projectId, newest.runId); console.log(edition.item); } }

tools

The free scan tools — the same public scans the MCP server exposes. These don’t take a projectId; they run against a URL you give them. Three return their result directly; the other three are asynchronous scans you start and then poll.

Direct (one call, result inline):

MethodParametersReturnsEndpoint
cl.tools.sitemapVisualizer(body)body{ item: SitemapVisualizerToolResponse }POST /v1/tools/sitemap-visualizer
cl.tools.aiCrawlerChecker(body)body{ item: AiCrawlerCheckerToolResponse }POST /v1/tools/ai-crawler-checker
cl.tools.fetchUrl(body)body: { url, bodyNeeded?, headersNeeded?, cleanHtml?, maxTimeoutMs?, bodyMaxBytes? }{ item: FetchUrlToolResponse }POST /v1/tools/fetch-url

On fetchUrl, branch on headersAvailable — never on whether headers exists. headers is optional in the type because it is genuinely absent when you pass headersNeeded: false, so its presence answers “did you ask for headers”, not “did we get any”. When you did ask and the target revealed nothing, headers arrives as {} with headersAvailable: false: a measurement, not a gap.

Async scans (start, then poll — see the Quickstart):

MethodParametersReturnsEndpoint
cl.tools.techStack.startScan(body)body (e.g. { domain }){ item: TechStackScanResponse }POST /v1/tools/tech-stack/scans
cl.tools.techStack.getScan(scanId)scanId: string{ item: TechStackScanResponse }GET /v1/tools/tech-stack/scans/{scanId}
cl.tools.trustSignals.startScan(body)body (e.g. { domain }){ item: TrustSignalsScanResponse }POST /v1/tools/trust-signals/scans
cl.tools.trustSignals.getScan(scanId)scanId: string{ item: TrustSignalsScanResponse }GET /v1/tools/trust-signals/scans/{scanId}
cl.tools.agentAdoption.startScan(body)body (e.g. { domain }){ item: AgentAdoptionScanResponse }POST /v1/tools/agent-adoption/scans
cl.tools.agentAdoption.getScan(scanId)scanId: string{ item: AgentAdoptionScanResponse }GET /v1/tools/agent-adoption/scans/{scanId}

A scan response carries status'queued' | 'running' | 'completed' | 'failed' — with result present once completed and error present if failed. Scan IDs expire after 24 hours. Of the scan request bodies, only fetchUrl’s is strongly typed; the scanners accept an open object where the domain field is what they read.

Full method map

All 36 methods, at a glance:

health check projects list · get competitors list · get aiVisibility dashboard · history · checkDetail · trend positioning dashboard · history · runDetail pricing dashboard · history · runDetail content dashboard · history · runDetail · changelog techTrust dashboard · history · runDetail alerts list schedules list strategicBriefing get · history · edition tools sitemapVisualizer · aiCrawlerChecker · fetchUrl techStack.{startScan,getScan} trustSignals.{startScan,getScan} agentAdoption.{startScan,getScan}

For the underlying HTTP — status codes, full request and response schemas, error codes — see the REST API reference. The SDK is a typed layer over exactly those endpoints.

Last updated on