CompetLab REST API
What it is
The CompetLab REST API is the programmatic surface for your competitive intelligence. It
returns the same data you see in the app, as JSON, so you can pull it into your own backend, a
scheduled job, or a dashboard you build. Almost all of it only reads. The one place it writes is
the Strategic Tickets board: its routes create, update, move and delete tickets, comments and
labels, and each write needs a read_write key (a read key lists and reads). The board works
on any plan and after one ends. The free scan tools are POST endpoints that fetch or scan a site
on CompetLab’s side, so they need a plan (402 subscription_required without one) — the quick
ones (sitemap, AI-crawler, fetch-url) return their result inline, and the three longer scans
(tech-stack, trust-signals, agent-adoption) return a scan you poll.
Reach for the REST API when the caller is your code. If the caller is an AI agent, the MCP server wraps this same API as agent-callable tools — pick the surface that matches who’s calling.
Base URL and versioning
Every request goes to https://api.competlab.com, and every route is versioned in the path:
https://api.competlab.com/v1/projectsThe version lives in the URL (/v1), so a future /v2 can ship without breaking /v1
callers.
Authentication
Every request needs a CL-API-Key header carrying a CompetLab API key (it starts with
cl_live_ and is 40 characters). You create keys in the dashboard under Settings → API
Keys. The full story — creating keys, the format, and auth errors — is in
Authentication.
curl https://api.competlab.com/v1/projects \
-H "CL-API-Key: YOUR_COMPETLAB_API_KEY"Resources
The API mirrors the product: your projects and competitors, the six monitored dimensions, your alerts and schedules, the Strategic Briefing, the Strategic Tickets board, and the free scan tools — 54 endpoints in all.
| Group | Endpoints |
|---|---|
| Projects | GET /v1/projects · GET /v1/projects/{projectId} |
| Competitors | GET …/competitors · GET …/competitors/{competitorId} |
| AI Visibility | GET …/ai-visibility · /history · /history/{checkId} · /trend |
| AI Sources | GET …/ai-sources · /history · /history/{checkId} |
| Positioning · Pricing · Tech & Trust | each: GET …/{dimension} · /history · /history/{runId} |
| Content | the three above, plus GET …/content/changelog |
| Alerts · Schedules | GET …/alerts · GET …/schedules |
| Strategic Briefing | GET …/strategic-briefing · /history · /history/{runId} |
| Strategic Tickets | GET / POST …/tickets · GET / PATCH / DELETE …/tickets/{ticketId} · PATCH …/{ticketId}/move · comments, labels and assignees — writes need a read_write key |
| Free Tools | POST /v1/tools/* runs a tool (sitemap, AI-crawler, fetch-url return inline); the tech-stack / trust-signals / agent-adoption scans add GET …/scans/{scanId} to poll |
The complete, per-endpoint reference is generated from the spec in the
API Reference. The live OpenAPI document is public at
https://api.competlab.com/v1/docs/openapi.json, with Swagger UI at
https://api.competlab.com/v1/docs.
Response shape
Responses are consistently wrapped:
- A single resource returns
{ "item": { … } }. - A collection returns
{ "items": [ … ] }; paginated collections add apaginationobject withpage,limit,total,totalPages, andhasMore. - Errors return
{ "error": { "code": "…", "message": "…", "status": 400 } }, wherecodeis a machine-readable snake_case string.
Long lists come a page at a time
Six reads can return large payloads: the AI Visibility dashboard, history and check detail, the AI
Sources dashboard and check detail, and the Tech & Trust dashboard. Each takes view, and /v1
answers compact unless you send view=full.
- Compact returns one page of each long list —
summary.marketMap.brands, AI Sources’summary.pagesandsummary.brands, a history row’scompetitorRankings— with a…Pageobject beside it (offset,limit,total,hasMore). Your own row is always on the page, and on the AI Visibility market map and history so is every tracked competitor’s — on every page, so de-duplicate by domain when you read more than one. An AI Sources core host lists its pages aspageUrls, and Tech & Trust states each crawler’s facts once, incrawlerCatalog. - Full returns every row of every list, as stored.
Both views open with readingGuide — the rule for reading each field the response carries, keyed
by its path. A check’s detail read for its answers (includeAnswers=true) leaves summary out
unless you send includeSummary=true or a paging parameter.
A loop over summary.marketMap.brands or summary.pages reads one page of it. Check hasMore on
the list’s …Page object and page on with the endpoint’s offset and limit, or send view=full.
Quote total as how many there are, never the length of the page.
Three combinations are refused with 400: a paging parameter beside view=full
(paging_requires_compact_view), a paging parameter beside includeSummary=false
(paging_requires_summary), and includeSummary=false without includeAnswers=true
(nothing_to_return).
null means unmeasured
One rule runs through every dimension, and it is the single most important thing to get right when you build on this data:
null means we did not measure it — never zero, never empty, never “no”. A measured 0
or false is reported as itself and is a real finding.
So hasFreePlan: false means we read the pricing page and there is no free plan.
hasFreePlan: null means we could not read the page at all. The two are different facts and
the API will not blur them: it would rather tell you nothing than tell you something it never
checked.
This matters because the failure is silent. if (!hasFreePlan) treats “we couldn’t read it”
and “they have no free plan” identically, and produces a confident sentence about a
competitor from a measurement that never happened. Branch on null explicitly, and say
“not measured” when you find it:
if (plan.hasFreePlan === null) {
// We didn't measure this — say so, or say nothing.
} else if (plan.hasFreePlan === false) {
// We measured it. There is no free plan. That's a finding.
}The same rule applies to rates and counts: a null rate had nothing to compute from, so plot
it as a break in the line rather than a zero. Per-field notes throughout the
API Reference tell you what each null specifically means.
One exception: a rank
A rank is null on a brand no answer named — rankByPresence on the AI Visibility market map and
the AI Sources brand list, and rank and rankChange on the AI Visibility trend. Brands are
ordered by how often they are named, so a brand named in no answer has no place in that order. That
null sits beside a measured answersNaming: 0: read it as not named in any answer — never
“not measured”, never a place, never a fall. (Under a provider filter the trend’s rank is
null for a different reason: a rank exists only across every model.)
An omitted key is a third state
Some newer fields extend that rule rather than repeat it. Where null says we tried and could
not measure this, an omitted key says reporting any value here would assert something we
never established.
Tech & Trust’s aiAccess works this way. When a competitor’s robots.txt could not be read,
assistantAccess and modelTrainingAccess are absent from the response, not empty — because
[] would claim we evaluated all six assistants and none can reach the site, which is a
completely different finding from “we couldn’t read the file.”
assistantAccess ?? [] reintroduces exactly the bug this shape exists to prevent. Branch on the
sibling measurement.status — measured, measured_no_policy_found, or could_not_measure —
before you read the arrays at all.
Note that measured_no_policy_found is a measurement: the site publishes no robots.txt, which
under the robots exclusion standard permits every crawler, so the verdicts render and are all
open. Only could_not_measure withholds them.
Rate limits
The free-tool and Strategic Tickets endpoints are rate-limited per API key; the core resource
endpoints aren’t throttled today. Over-limit requests get a 429. See
Rate limits for the numbers.
Next steps
- Quickstart → — your first request in a minute.
- Authentication → — keys, format, and errors.
- API Reference → — every endpoint, generated from the spec.
FAQ
What can the CompetLab REST API do?
It's an API over your competitive intelligence. With a CompetLab API key you can pull your projects and competitors, the six monitored dimensions (AI Visibility, AI Sources, Positioning, Pricing, Content, and Tech & Trust), your alerts and schedules, and your Strategic Briefing — all as JSON. It also exposes the free scan tools as POST endpoints, and it reads and writes your project's Strategic Tickets board. It returns the same data the dashboards and the MCP server use, so it's the surface you reach for when the caller is your own backend, a scheduled job, or a dashboard you build.
How do I authenticate?
With a CompetLab API key sent as a CL-API-Key header on every request. Keys start with cl_live_ and are 40 characters long, and you create them in the dashboard under Settings → API Keys (you'll need the Owner or Admin role). A key maps to your organization, so the API returns exactly what your organization can see. There's no OAuth or login step — the key travels with each request.
How is the REST API different from the MCP server?
Same data, different consumer. The REST API is for your own code — server-to-server automation, scheduled jobs, dashboards. The MCP server wraps this same API as tools an AI agent can discover and call inside a conversation. Under the hood the MCP server calls the REST API, so anything you can do through one you can do through the other; pick the surface that matches who's calling.
Is there an OpenAPI spec?
Yes. The live OpenAPI document is public at https://api.competlab.com/v1/docs/openapi.json, and interactive Swagger UI is at https://api.competlab.com/v1/docs. The per-endpoint API Reference in these docs is generated from that spec, so it stays in sync with what the API actually accepts and returns.
Can I write or change data through the API?
Only on the Strategic Tickets board. Its routes create, update, move and delete tickets, write comments and manage labels, and each write needs a read_write API key — a read key can list and read tickets but is refused on writes with 403 insufficient_scope. The board works on any plan and after one ends. Nothing else changes through the API: there's no way to change your account, projects, competitors, alerts, schedules or settings. The free scan tools are POST endpoints too, but they run against a public URL you provide and don't modify your CompetLab data — they fetch or scan a site on CompetLab's side, so they need a plan (402 subscription_required without one): three return their result immediately (sitemap, AI-crawler, fetch-url) and three start a scan you poll (tech-stack, trust-signals, agent-adoption).