Skip to Content
DocsDevelopersSDKOverview

CompetLab TypeScript SDK

What it is

The SDK is the typed TypeScript path to CompetLab. It wraps the same REST API the dashboards use, so every method maps to a real endpoint and every response comes back as a typed object — your projects, competitors, the five monitored dimensions, your alerts and schedules, the Strategic Briefing, and the free scan tools. You write cl.pricing.dashboard(projectId); the SDK builds the request, sends your key, checks the status, and hands you back a typed result.

Pick the SDK when you’re building on CompetLab in TypeScript or Node.js — a backend service, a scheduled job, an internal tool. It’s the same data as the REST API and the MCP server, aimed at a different caller: the REST API is for any language over plain HTTP, the MCP server is for AI agents that discover and call tools mid-conversation, and the SDK is for TypeScript code that wants types and autocomplete instead of raw HTTP. Under the hood all three read the same backend.

Install

npm install @competlab/sdk

It’s published on npm as @competlab/sdk, MIT-licensed, with no runtime dependencies — it uses the native fetch built into Node.js 20+. The package ships both ESM and CommonJS builds with TypeScript declarations for each, so both import CompetLab from '@competlab/sdk' and const CompetLab = require('@competlab/sdk').default work, with types either way. (The client is the default export, so CommonJS reaches it via .default.)

Quick start

import CompetLab from '@competlab/sdk'; const cl = new CompetLab({ apiKey: process.env.COMPETLAB_API_KEY! }); // See how ChatGPT, Claude, and Gemini mention your brand versus competitors const { data } = await cl.aiVisibility.dashboard('65a1b2c3d4e5f6a7b8c9d0e1'); console.log(data.item.summary);

That’s the whole shape of it: construct once with a key, then call a method. The Quickstart walks the first request end to end — including why data is destructured and how failures surface.

The surface

The client is organized into 12 resources that mirror the API. Each method is a thin, typed call over one endpoint:

ResourceMethodsWhat you reach
health1Service health check (the one call that needs no key).
projects2List your projects; get one project’s setup and freshness.
competitors2List a project’s competitors; get one competitor’s detail.
aiVisibility4AI Visibility dashboard, history, one check’s detail, and provider trend.
positioning3Positioning dashboard, history, and one run’s detail.
pricing3Pricing dashboard, history, and one run’s detail.
content4Content dashboard, history, run detail, and the content changelog.
techTrust3Tech & Trust dashboard, history, and one run’s detail.
alerts1List a project’s alerts, filtered by dimension and severity.
schedules1List a project’s monitoring schedules.
strategicBriefing3Pull the Strategic Briefing, list past editions, and read one edition in full.
tools9The free scans — sitemap, AI-crawler, URL fetch, and start/get for the tech-stack, trust-signals, and agent-adoption scans.

That’s 36 methods in total. Every one, with its parameters and return type, is in the SDK reference.

Authentication

Pass your CompetLab API key when you construct the client — it’s the one required option:

const cl = new CompetLab({ apiKey: 'cl_live_...' });

The SDK sends that key on every request as a CL-API-Key header — the same header the REST API expects. Keys start with cl_live_ and are 40 characters; create and manage them in your CompetLab organization settings. Two honest notes:

  • The SDK does not read environment variables for you. The apiKey option is required and has no fallback — if you keep your key in COMPETLAB_API_KEY, you pass it in yourself (process.env.COMPETLAB_API_KEY!). Nothing is read from the environment behind your back.
  • The only other option is baseUrl, which defaults to https://api.competlab.com. You’d override it only to point at a different environment; almost no one needs to.

Responses and types

Every method returns the parsed response alongside the raw request and response objects, so you destructure data:

const { data } = await cl.projects.list(); // data.items => ProjectListItemResponse[]

Responses follow the REST API’s envelopes, and the types match:

  • A single resource comes back as { item: ... }.
  • A list comes back as { items: [...] }.
  • A paginated list adds { items, pagination }, where pagination is { page, limit, total, totalPages, hasMore }. Paging is manual — pass { page, limit } and loop on hasMore; there’s no auto-paging iterator.
  • The Strategic Briefing is the one exception: it returns { item, meta, coverage, contains }, and you branch on meta.status before reading item.

All response types are exported from the package, so you can import and reference them directly. data is always present on a returned result — the client throws on any non-2xx response, so a call that returns has succeeded. You need no ! assertion and no if (result.error) check; use try/catch. The Quickstart shows the pattern in context.

null means unmeasured

The same rule as the REST API: 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.

The types carry it, but TypeScript can’t stop you flattening it — if (!plan.hasFreePlan) is true for both false and null, and ?? or || turns a null straight back into the placeholder the API stopped sending. So the SDK exports three guards:

import { isMeasured, isMeasuredTrue, isMeasuredFalse } from '@competlab/sdk'; if (isMeasuredFalse(plan.hasFreePlan)) { // We read the pricing page. There is no free plan. That's a finding. } else if (!isMeasured(plan.hasFreePlan)) { // We couldn't read it — say "not measured", or say nothing. }

isMeasured narrows away the null; isMeasuredTrue and isMeasuredFalse assert a measured value. Reach for them anywhere a nullable boolean or number reaches a sentence a customer reads.

An omitted key is a third state

null says we tried and could not measure this. An omitted key says something else: that reporting any value at all would assert something we never established. Tech & Trust’s aiAccess works this way, and no guard protects you here — the field simply isn’t there.

When a competitor’s robots.txt couldn’t be read, assistantAccess and modelTrainingAccess are absent rather than empty, because [] would claim we evaluated all six assistants and none can reach the site. That’s a different finding from “we couldn’t read the file”.

c.aiAccess?.assistantAccess ?? [] reintroduces exactly the bug the shape prevents. Branch on measurement.status before you touch the arrays.

for (const c of data.item.competitors) { if (!c.aiAccess) continue; // no AI-access section at all if (c.aiAccess.measurement.status === 'could_not_measure') continue; // no verdicts exist // 'measured' and 'measured_no_policy_found' both carry real verdicts — the second // is a site with no robots.txt, which under the standard allows every crawler. for (const a of c.aiAccess.assistantAccess ?? []) { console.log(a.assistantName, a.crawlerAccessStatus); } }

Two honest notes on the types, both true as of 4.0.0:

  • crawlerAccessStatus, trainingAccessStatus, measurement.status, crawlerPurpose and honoursRobotsTxt are typed string, not unions — the API publishes them without an enum, so a switch over them gets no exhaustiveness check and a mistyped case fails silently at runtime. Narrow them yourself with an as const array and a type guard. The free tools’ equivalents do carry unions, so the asymmetry is real rather than something you’re misreading.
  • 4.0.0 is a breaking release. allowsAiAccess, blockedAiBotsCount and aiBotsBlocked were deleted, not deprecated, and there is deliberately no replacement boolean — one flag can’t say whether AI can reach a site. If you read any of them, your build breaks, which is the point.

Errors

Every failure throws a single typed error, CompetLabError:

import CompetLab, { CompetLabError } from '@competlab/sdk'; try { const { data } = await cl.projects.get('does-not-exist'); } catch (err) { if (err instanceof CompetLabError) { console.error(err.status, err.code, err.message); // e.g. 404 "project_not_found" "Project not found" } }

CompetLabError carries the HTTP status (number), a snake_case code (string) matching the REST API’s error codes, and a human-readable message. Network and parse failures throw the same class with code: "network_error". There’s one error class, not a hierarchy — you branch on status or code.

What it doesn’t do

The SDK is deliberately a thin, predictable client — no hidden magic between your call and the wire. That’s a design choice, not a gap: you stay in control of retries, timing, and paging rather than inheriting a framework’s opinions. Concretely:

  • It doesn’t retry or time out for you. Each call is a single request. Wrap it yourself if you want retry or timeout behavior — the SDK won’t second-guess your policy.
  • It doesn’t auto-paginate. Pagination is explicit via { page, limit } and the pagination object, so you decide when to fetch the next page.
  • No environment-variable magic, streaming, webhooks, or CLI. You pass the key explicitly, and there’s no bundled command-line tool — the SDK is a typed request layer, nothing more.
  • It reads. The API surface the SDK covers is read-first. The only “writes” are the three asynchronous scan tools — tech stack, trust signals, and agent adoption — which start a live scan of a URL you give them and return an ID you poll. The other three free tools (sitemap, AI-crawler, and URL fetch) are plain reads that return inline. Nothing in the SDK changes your CompetLab projects, alerts, or settings.

Next steps

  • Quickstart → — install, construct, your first call, and the async-scan poll loop, end to end.
  • SDK reference → — all 36 methods, grouped by resource, with parameters, return types, and the endpoint each maps to.

FAQ

What is @competlab/sdk?

It's the official TypeScript SDK for the CompetLab REST API — a typed client you install with npm install @competlab/sdk. Instead of writing fetch calls, you construct a client with your API key and call typed methods like cl.pricing.dashboard(projectId). It covers the entire API: 36 methods across 12 resources, spanning your projects, competitors, the five monitored dimensions, alerts, schedules, the Strategic Briefing, and the free scan tools. It ships full TypeScript types, throws a typed CompetLabError on failure, has zero runtime dependencies, and runs on Node.js 20 or newer.

How do I install and authenticate?

Run npm install @competlab/sdk, then construct the client with your API key: new CompetLab({ apiKey: 'cl_live_...' }). The key is the one required option. The SDK sends it on every request as a CL-API-Key header — the same header the REST API expects. Keys start with cl_live_ and are 40 characters; you create them in your CompetLab organization settings. The SDK does not read environment variables for you, so if your key lives in COMPETLAB_API_KEY you pass it in explicitly.

How is the SDK different from the REST API and the MCP server?

Same data, different caller. The REST API is plain HTTP for any language. The MCP server is for AI agents that discover and call tools mid-conversation. The SDK is for TypeScript and Node.js code that wants types and autocomplete instead of raw HTTP — it's a thin typed layer over the same REST API, so anything you can do with one you can do with the others. Pick the surface that matches who's calling: HTTP client, AI agent, or TypeScript code.

What does a method return?

Each method returns the parsed response, which you destructure as data. Responses follow the REST API's envelopes: a single resource is { item }, a list is { items }, and a paginated list adds { items, pagination } with page, limit, total, totalPages, and hasMore. The Strategic Briefing is the exception — it returns { item, meta, coverage, contains }, and you check meta.status before reading item. All response types are exported from the package, so you can import and reference them directly.

Do I need a non-null assertion on data?

No. data is always present on a returned result, and the type says so. The client throws on any non-2xx response, so a call that returns has succeeded — there is no success path where data is missing. Earlier versions declared an error branch the runtime never produced, which forced a ! assertion or a dead if (result.error) check; both are gone as of v3. Handle failures with try/catch and a typed CompetLabError. Note this is separate from null field values inside data: null there means CompetLab did not measure that field, never that the value is zero or false, and the isMeasured guards are the safe way to branch on it.

How do errors work?

Every failure throws a single typed error, CompetLabError, which you catch and inspect. It carries the HTTP status as a number, a snake_case code string that matches the REST API's error codes (like project_not_found or api_key_invalid), and a human-readable message. Network and parse failures throw the same class with code "network_error". There's no error hierarchy — one class — so you branch on status or code.

Does it retry, time out, or auto-paginate?

No — the SDK is a typed request layer, not a framework. Each call is a single request with no automatic retries and no built-in timeout, so if you need those you wrap the call yourself. Pagination is manual too: pass { page, limit } and loop on the pagination object's hasMore flag. There's no auto-paging iterator, no streaming, no webhooks, and no bundled CLI. Keeping it thin is deliberate — it's the same behavior as calling the REST API directly, just typed.

Which runtimes and module systems does it support?

It requires Node.js 20 or newer, because it uses the native fetch built into that version rather than shipping a polyfill. The package publishes both ESM and CommonJS builds with matching TypeScript declarations, so both import CompetLab from '@competlab/sdk' and const CompetLab = require('@competlab/sdk').default work, with full types either way. The client is the default export, so CommonJS reaches it through .default. It has zero runtime dependencies and is tree-shakeable.

Last updated on