SDK Reference
Conventions
A few things hold for every method on this page:
- You destructure
data. Each call returns{ data, request, response };datais 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 }wherepaginationis{ page, limit, total, totalPages, hasMore }. Paging is manual. - Long lists inside a dimension come a page at a time. Six reads take
viewand answercompactunless you passview: 'full'; the sections below name them. See Long lists come a page at a time. - Errors throw. Any non-2xx response throws a
CompetLabErrorwithstatus,code, andmessage. It’s not a returned value; youcatchit. - Types are exported. Every response type named below (
ProjectDetailResponse,PaginationMeta, and the rest) is exported from@competlab/sdkfor 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.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.health.check() | — | { item: HealthResponse } | GET /v1/health |
projects
Your projects — the top of every other call, since most methods take a projectId.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
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.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
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
Who the AI models recommend in your category, and where you stand among them — dashboard, history, one check’s detail, and the trend digest.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.aiVisibility.dashboard(projectId, query?) | query?: { includeAnswers?, provider?, brand?, promptIndex?, view?, mapOffset?, mapLimit? } | { item: AiVisibilityDashboardResponse } | GET …/ai-visibility |
cl.aiVisibility.history(projectId, query?) | query?: { page?, limit?, view? } | { readingGuide, items: AiVisibilityHistoryItemResponse[], pagination, truncated } | GET …/ai-visibility/history |
cl.aiVisibility.checkDetail(projectId, checkId, query?) | query?: { includeAnswers?, provider?, brand?, promptIndex?, view?, mapOffset?, mapLimit?, includeSummary? } | { item: AiVisibilityCheckDetailResponse } | GET …/ai-visibility/history/{checkId} |
cl.aiVisibility.trend(projectId, query?) | query?: { dateFrom?, dateTo?, provider?, detail? } | { item: AiVisibilityTrendResponse } | GET …/ai-visibility/trend |
dashboard, checkDetail and history answer in the compact view unless you pass
view: 'full'. On the first two, summary.marketMap.brands is one page, with
summary.marketMap.brandsPage ({ offset, limit, total, hasMore }) saying where it sits; page
on with mapOffset and mapLimit. Your own row and every tracked competitor’s are on every
page, so a tracked competitor missing from it was named in no answer. A compact history row
keeps the first 10 of its competitorRankings plus every tracked competitor’s and your own, with
competitorRankingsPage counting the whole list. On checkDetail, summary is optional: a
read with includeAnswers: true leaves it out unless you pass includeSummary: true.
history adds a truncated boolean to the envelope, set when whole checks were dropped from the
end of items to fit the response; pagination.hasMore doesn’t count them, so lower limit and
read the page again. aiSources.history does the same.
rankByPresence on a map row is number | null: null on a brand named in no answer, which
reads not named in any answer — never “not measured”, never a place. See
One exception: a rank.
The provider filter takes an AiProvider, one of five members:
'openai' | 'claude' | 'gemini' | 'perplexity' | 'google_ai_overviews'. Note the wire value for
ChatGPT is 'openai' — cl.aiVisibility.trend(projectId, { provider: 'openai' }).
trend() returns a digest, not a plot: one item holding window, scope,
companies[] and events, where each company carries its reading now, its reading at the
start of the window, and the difference. Read presenceChangeSeparable before narrating any
of it — presenceChange is a number even when the movement sits inside the noise:
for (const c of data.item.companies) {
if (c.presenceChangeSeparable === true && (c.presenceChange ?? 0) > 0) {
report(`${c.name} is up`);
}
}Pass detail: 'series' for each company’s share downsampled to at most 12 points. Under a
provider scope, rank and score are null and enginesBacking is omitted — there is
no per-engine score, by design.
The rows are your company, every tracked competitor, and up to 3 companies you don’t track, and
every reading carries checksAnalysed — how many published checks its window pools. A company’s
score is a measured 0 on a check that named it nowhere, and its rank and rankChange are
null there.
Set includeAnswers: true on dashboard or checkDetail to also get the models’ raw answers
— every prompt sent, and every brand each model named, in the order it named them, with its stated reasoning.
Google AI Overviews is the exception: it answers in prose, so its entries carry a name and a
domain and none of the profile fields, and its rank is the order of first mention computed
by CompetLab — Google assigned no position, so never report it as one.
It’s a large block — 8 prompts across 5 models on every plan — and an entry is one brand a model named,
at about 1,500 characters, so read summary.totalEntries to size it first. 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.
aiSources
Which pages the searching engines opened to answer your buyers’ questions, and whether your site is among them — dashboard, history, and one check’s detail.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.aiSources.dashboard(projectId, query?) | query?: { includeAnswers?, engine?, promptIndex?, view?, pagesHost?, pagesOffset?, pagesLimit?, brandsOffset?, brandsLimit? } | { item: AiSourcesDashboardResponse } | GET …/ai-sources |
cl.aiSources.history(projectId, query?) | query?: { page?, limit? } | { items: AiSourcesHistoryItemResponse[], pagination, truncated } | GET …/ai-sources/history |
cl.aiSources.checkDetail(projectId, checkId, query?) | query?: { includeAnswers?, engine?, promptIndex?, view?, pagesHost?, pagesOffset?, pagesLimit?, brandsOffset?, brandsLimit?, includeSummary? } | { item: AiSourcesCheckDetailResponse } | GET …/ai-sources/history/{checkId} |
The engine filter takes 'perplexity' | 'google_ai_overviews' — the two engines that hand
back the pages they pulled while answering. Three rules govern every figure these return:
retrieved, never cited (an engine never says which pages it leaned on, so no count here is a
citation count); per engine, never pooled (the two read different pages, so adding their page
counts describes a list neither produced); and counts, never rates (report n of 8 answers,
never a percentage).
dashboard and checkDetail answer in the compact view unless you pass view: 'full':
summary.pages and summary.brands are one page each, with summary.pagesPage and
summary.brandsPage saying where each sits. Page on with pagesOffset / pagesLimit and
brandsOffset / brandsLimit, or narrow the pages to one host with pagesHost. Your own brand
row is on every page. pagesPage.total is a paging figure — rows of the list, across both
engines — never an engine’s count of pages retrieved; that count is per engine, on
summary.perEngine. Each summary.coreHosts[] lists its pages as pageUrls in place of
pages: read them as rows of summary.pages, with pagesHost set to that host. On
checkDetail, summary is optional: a read with includeAnswers: true leaves it out unless you
pass includeSummary: true.
rankByPresence on a summary.brands row is null on a brand no answer named: beside
answers.answersNaming: 0 it reads not named in any answer, never a place.
The sharpest trap is status on a core host, which has more than two values — filtering by
“not already named” sweeps in pages that were never read:
const hosts = data.item.summary.coreHosts;
// WRONG — turns a page we could not read into one that omits you
const wrong = hosts.filter((h) => h.status !== 'already_named');
// right: only 'missing' means we read the page and your name was not on it
const toWork = hosts.filter((h) => h.status === 'missing');actionHint.text on each core host and the sentences under summary.limits.sentences are
payload — render them verbatim rather than composing your own from the code beside them.
actionHint.code is typed string rather than a union, so a new code is not a breaking change
and gets no exhaustiveness check.
positioning
Where you sit in the market narrative — dashboard, history, and one run’s detail.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
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.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
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.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
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.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.techTrust.dashboard(projectId, query?) | query?: { view? } | { 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.
dashboard answers in the compact view unless you pass view: 'full'. Each crawler’s catalog
facts — what it is for (crawlerPurpose) and whether a robots.txt rule against it binds
(honoursRobotsTxt) — are stated once in crawlerCatalog, keyed by userAgentToken, instead of
on every verdict that names it, and an explanation that reads the same everywhere carries only
its code, with the sentence in explanationCatalog. view: 'full' repeats them on every item.
alerts
Notable changes surfaced across the dimensions, filterable.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
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,
ai-sources;
severity is one of critical, high, medium, info.
schedules
The monitoring cadence configured for a project.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.schedules.list(projectId) | projectId: string | { items: ScheduleItemResponse[] } | GET …/schedules |
strategicBriefing
The synthesized read across every dimension, every month or every two weeks on Process. Its envelope carries more than
{ item }.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.strategicBriefing.get(projectId, query?) | query?: { sections?, includeCharts? } | { meta, item, coverage, contains, tickets } | 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? } | { meta, item, coverage, contains, tickets } | 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', 'competitors']) and includeCharts: true for chart data.
An edition has no actions section, and 'actions' is not in the sections type. Every move in
the briefing lands on your Strategic Tickets board — as a new ticket, most important first, or on
the ticket already there for that work. tickets on the envelope says what the edition did to the
board: opened (the ids of the tickets it opened that are still on the board), commented (the
comments it wrote on tickets already there), alreadyOnBoard (the tickets its moves matched
instead of opening a second one) and recheckedUnchanged (open tickets it measured again and found
where they stood), with total and byStatus counting the opened tickets per column as the board
stands now. A ticket in neither commented nor recheckedUnchanged was not measured by this
edition — not checked, never unchanged. tickets is null unless meta.status is 'done'. Read
the tickets themselves with
cl.tickets.list(projectId, { origin: 'briefing', briefingRunId: data.meta.runId! }).
const { data } = await cl.strategicBriefing.get(projectId, {
sections: ['deep-ai-visibility', 'competitors'],
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);
}
}tickets
The project’s Strategic Tickets board — the one the team sees in the app, and the only part of
the API that changes a project. Every method works on any plan and after one ends, and every
method that writes needs a read_write key — a read key gets 403 insufficient_scope. What you write carries origin: 'api', so the
board tells your automation’s tickets and comments from a person’s.
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.tickets.list(projectId, query?) | query?: { page?, limit?, status?, sort?, closed?, origin?, dimension?, briefingRunId?, assignee?, labelId?, number?, q?, impactMin?, effort?, maxMinutes?, dueFrom?, dueBefore?, activeSince?, include? } | { items: TicketListItemResponse[], pagination, byStatus } | GET …/tickets |
cl.tickets.get(projectId, ticketId) | ticketId: string | { item: TicketResponse } | GET …/tickets/{ticketId} |
cl.tickets.create(projectId, body) | body: { title, status, description?, labelIds?, assigneeUserId?, dueDate?, effort?, impact? } | { item: TicketResponse } | POST …/tickets |
cl.tickets.update(projectId, ticketId, body) | body: { title?, description?, labelIds?, assigneeUserId?, dueDate?, effort?, impact? } | { item: TicketResponse } | PATCH …/tickets/{ticketId} |
cl.tickets.move(projectId, ticketId, body) | body: { status, beforeId?, afterId?, position? } | { item: TicketResponse, placement } | PATCH …/tickets/{ticketId}/move |
cl.tickets.delete(projectId, ticketId) | ticketId: string | { item: TicketDeletedResponse } | DELETE …/tickets/{ticketId} |
cl.tickets.assignees(projectId) | projectId: string | { items: TicketPersonResponse[] } | GET …/tickets/assignees |
cl.tickets.comments.list(projectId, ticketId) | ticketId: string | { items: TicketCommentResponse[] } | GET …/tickets/{ticketId}/comments |
cl.tickets.comments.create(projectId, ticketId, body) | body: { body } | { item: TicketCommentResponse } | POST …/tickets/{ticketId}/comments |
cl.tickets.comments.update(projectId, ticketId, commentId, body) | body: { body } | { item: TicketCommentResponse } | PATCH …/tickets/{ticketId}/comments/{commentId} |
cl.tickets.comments.delete(projectId, ticketId, commentId) | commentId: string | { item: TicketDeletedResponse } | DELETE …/tickets/{ticketId}/comments/{commentId} |
cl.tickets.labels.list(projectId) | projectId: string | { items: TicketLabelResponse[] } | GET …/tickets/labels |
cl.tickets.labels.create(projectId, body) | body: { name, color } | { items: TicketLabelResponse[] } | POST …/tickets/labels |
cl.tickets.labels.update(projectId, labelId, body) | body: { name?, color? } | { items: TicketLabelResponse[] } | PATCH …/tickets/labels/{labelId} |
cl.tickets.labels.delete(projectId, labelId) | labelId: string | { items: TicketLabelResponse[] } | DELETE …/tickets/labels/{labelId} |
list answers a page at a time — limit defaults to 50, at most 100 — with pagination.total
counting every match and byStatus the matches per column, whatever status you passed. sort
is 'board' (the default: the columns in board order, each in the order the team keeps, and the
only order to read neighbours for a move from), 'priority', 'due' or 'activity'. closed
defaults to 'all', and q matches the title or the description. A row is a
TicketListItemResponse: every field but description, which include: ['description'] adds;
get always carries it.
const { data: page } = await cl.tickets.list(projectId, { status: ['triage', 'todo'], sort: 'priority' });
// page.pagination => { page: 1, limit: 50, total: 23, totalPages: 1, hasMore: false }
// page.byStatus => { triage: 9, todo: 14, in_progress: 3, done: 11, dismissed: 2 }
const { data: created } = await cl.tickets.create(projectId, {
title: 'Answer the pricing-page objection Acme now leads with',
status: 'todo',
});
// Name its neighbours (beforeId above, afterId below) or a position, not both.
// Naming nothing puts it at the bottom of the column.
const { data: moved } = await cl.tickets.move(projectId, created.item.id, {
status: 'in_progress',
position: 'top',
});
// moved.placement => { status: 'in_progress', above: null, below: { id, number: 12, title }, ignored: [] }numberis a name, not an address. It’s the ticket’s number on the board (#14in the app):list(projectId, { number: 14 })finds it, and every method that acts on a ticket takes itsid.movesays where the ticket landed.placementcarries its neighbours now,aboveandbelow, andignored[]— each neighbour you named that was not used, and why. A ticket changes column only throughmove, never throughupdate.- On
update,nullclears a field and an omitted field is left alone; the description clears with"", labels with[]. - A ticket a Strategic Briefing opened can’t be deleted. Read
deletable, andmoveit todismissedinstead. - A briefing’s ticket carries
briefing: the edition’srunId; thedimensionand the edition’s estimate of the work (estimatedMinutes), each only where the edition gave it — anullestimate is unsized, never quick; andextendsTicketId, the ticket already on the board it builds on. It’snullon a ticket a person or a key opened. - A comment carries
briefingtoo —{ runId, kind, editionNumber, completedAt }on one an edition wrote,nullon every other.kindis aTicketCommentKind('result','basis_changed','basis_weaker','basis_stronger','basis_gone'), ornullon a kind the API doesn’t know yet — the comment is still there. Its body is dated facts: report it as the comment states it, never as the fix having worked. - A comment a person wrote in the app can’t be edited through the API —
403 forbidden, whatever the key. - Labels are the project’s own. A ticket names them by ID from
labels.list, and a label write answers with the project’s whole label list.
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. All six fetch or scan a
site on CompetLab’s side, so the key’s organization needs a plan — without one the call fails with
402 subscription_required and nothing runs; a finished scan’s get* read still answers.
Direct (one call, result inline):
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.tools.sitemapVisualizer(body) | body: { domain, sitemapUrl?, includeUrls? } | { item: SitemapVisualizerToolResponse } | POST /v1/tools/sitemap-visualizer |
cl.tools.aiCrawlerChecker(body) | body: { domain, industry? } | { 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):
| Method | Parameters | Returns | Endpoint |
|---|---|---|---|
cl.tools.techStack.startScan(body) | body: { 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: { 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: { domain, debugMode?, includeFixPrompts? } | { 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. Every tool’s request body is typed; where it takes a domain, that is a bare hostname or
a full URL.
Full method map
All 54 methods, at a glance:
health check
projects list · get
competitors list · get
aiVisibility dashboard · history · checkDetail · trend
aiSources dashboard · history · checkDetail
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
tickets list · get · create · update · move · delete · assignees
comments.{list,create,update,delete}
labels.{list,create,update,delete}
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.