AI Visibility
REST endpoints for AI Visibility. Base URL https://api.competlab.com; every request needs a CL-API-Key header (see Authentication). Responses wrap in { item } or { items }; errors in { error }.
Get latest AI visibility data
/v1/projects/{projectId}/ai-visibilityGet 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=<domain>, 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.
Path parameters
| Name | Type | Description |
|---|---|---|
projectId | string | Project ID |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
includeAnswers | boolean | 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. Default false. | |
provider | string | Return only this model’s answers. Requires includeAnswers=true. Does not change any number under summary, and does not narrow providerStatus. Values: openai, claude, gemini. | |
brand | string | 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. | |
promptIndex | number | 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. |
Request
curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/ai-visibility?includeAnswers=false&provider=openai&brand=competitor.com&promptIndex=0" \
-H "CL-API-Key: YOUR_COMPETLAB_API_KEY"Response 200 OK
Example
{
"item": {
"lastUpdatedAt": "2026-03-15T10:00:00.000Z",
"summary": {
"customer": {
"domain": "mycompany.com",
"mentionRate": 66.7,
"avgRank": 2.5,
"mentionCount": 6,
"aiScore": 73,
"perProvider": {
"openai": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
},
"claude": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
},
"gemini": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
}
},
"perPrompt": [
{
"promptIndex": 0,
"promptLabel": "best sales forecasting platforms for mid-market teams",
"mentionedBy": [
"openai",
"claude"
],
"score": 61
}
]
},
"topCompetitor": {
"domain": "competitor.com",
"name": "Competitor Inc",
"mentionRate": 88.9,
"avgRank": 1.8
},
"mentionRateGap": -22.2,
"totalCompetitorsFound": 8,
"totalQueries": 9,
"totalEntries": 45,
"competitorRankings": [
{
"domain": "competitor.com",
"name": "Competitor Inc",
"isOwn": false,
"openaiAvgRank": 2,
"claudeAvgRank": 1.7,
"geminiAvgRank": 3,
"overallAvgRank": 2.2,
"mentionCount": 7,
"mentionRate": 77.8,
"aiScore": 68,
"isTracked": false
}
]
},
"latestCheckDataAvailable": {
"available": false,
"reason": "incomplete_coverage",
"measuredAnswers": 8,
"expectedAnswers": 9
},
"answers": [
{
"provider": "openai",
"promptIndex": 0,
"promptText": "best sales forecasting platforms for mid-market teams",
"brands": [
{
"rank": 1,
"name": "Competitor Inc",
"domain": "competitor.com",
"description": "Cloud platform for pipeline forecasting aimed at mid-market sales teams.",
"rankingRationale": "Listed first because it integrates directly with the major CRMs.",
"sentiment": "recommended",
"mentionContext": "direct_recommendation",
"targetAudience": "Mid-size B2B sales teams",
"pricingSignal": "mid_range",
"positionConfidence": 0.9,
"features": [
"pipeline forecasting",
"CRM sync"
],
"differentiation": {
"axis": "technology",
"uniqueValue": "Only platform with native two-way CRM sync"
},
"messaging": {
"keywords": [
"automation",
"mid-market",
"self-serve"
],
"credibilitySignals": [
"SOC 2 Type II",
"used by 400+ teams"
],
"differentiationClaims": [
"fastest setup in category"
]
}
}
]
}
],
"unansweredQueries": [
{
"provider": "gemini",
"promptIndex": 2,
"promptText": "best sales forecasting platforms for mid-market teams",
"reason": "no_usable_answer"
}
],
"providerStatus": {
"openai": {
"reported": true,
"completedAt": "2026-03-15T10:00:00.000Z",
"answersCounted": 3,
"brandsNamed": 21
},
"claude": {
"reported": true,
"completedAt": "2026-03-15T10:00:00.000Z",
"answersCounted": 3,
"brandsNamed": 21
},
"gemini": {
"reported": true,
"completedAt": "2026-03-15T10:00:00.000Z",
"answersCounted": 3,
"brandsNamed": 21
}
},
"answerCoverage": {
"queriesSent": 9,
"answersCounted": 8
},
"answersTruncated": false
}
}Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameters · invalid_run_id · invalid_check_id | Bad request — the payload failed validation. |
| 401 | api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scope | The CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope. |
| 404 | project_not_found · no_data_available | No project matches the id in the path. |
Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.
{
"error": {
"code": "invalid_parameters",
"message": "Domain is required",
"status": 400
}
}Get AI visibility check history
/v1/projects/{projectId}/ai-visibility/historyGet 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.
Path parameters
| Name | Type | Description |
|---|---|---|
projectId | string | Project ID |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
page | number | Page number (1-indexed). Default 1. Min 1. | |
limit | number | Number of items per page. Default 20. Range 1–100. |
Request
curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/ai-visibility/history?page=1&limit=20" \
-H "CL-API-Key: YOUR_COMPETLAB_API_KEY"Response 200 OK
Example
{
"items": [
{
"checkId": "507f1f77bcf86cd799439011",
"completedAt": "2026-03-15T10:00:00.000Z",
"summary": {
"customer": {
"domain": "mycompany.com",
"mentionRate": 66.7,
"avgRank": 2.5,
"mentionCount": 6,
"aiScore": 73,
"perProvider": {
"openai": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
},
"claude": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
},
"gemini": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
}
},
"perPrompt": [
{
"promptIndex": 0,
"promptLabel": "best sales forecasting platforms for mid-market teams",
"mentionedBy": [
"openai",
"claude"
],
"score": 61
}
]
},
"topCompetitor": {
"domain": "competitor.com",
"name": "Competitor Inc",
"mentionRate": 88.9,
"avgRank": 1.8
},
"mentionRateGap": -22.2,
"totalCompetitorsFound": 8,
"totalQueries": 9,
"totalEntries": 45,
"competitorRankings": [
{
"domain": "competitor.com",
"name": "Competitor Inc",
"isOwn": false,
"openaiAvgRank": 2,
"claudeAvgRank": 1.7,
"geminiAvgRank": 3,
"overallAvgRank": 2.2,
"mentionCount": 7,
"mentionRate": 77.8,
"aiScore": 68,
"isTracked": false
}
]
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 47,
"totalPages": 3,
"hasMore": true
},
"truncated": false
}Paginated — pass page and limit query parameters and follow pagination.hasMore to page through the full set.
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scope | The CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope. |
Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.
{
"error": {
"code": "api_key_invalid",
"message": "Invalid API key",
"status": 401
}
}Get AI visibility data for a specific check
/v1/projects/{projectId}/ai-visibility/history/{checkId}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=<domain> (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.
Path parameters
| Name | Type | Description |
|---|---|---|
checkId | string | Check ID |
projectId | string | Project ID |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
includeAnswers | boolean | 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. Default false. | |
provider | string | Return only this model’s answers. Requires includeAnswers=true. Does not change any number under summary, and does not narrow providerStatus. Values: openai, claude, gemini. | |
brand | string | 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. | |
promptIndex | number | 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. |
Request
curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/ai-visibility/history/507f1f77bcf86cd799439012?includeAnswers=false&provider=openai&brand=competitor.com&promptIndex=0" \
-H "CL-API-Key: YOUR_COMPETLAB_API_KEY"Response 200 OK
Example
{
"item": {
"checkId": "507f1f77bcf86cd799439011",
"completedAt": "2026-03-15T10:00:00.000Z",
"summary": {
"customer": {
"domain": "mycompany.com",
"mentionRate": 66.7,
"avgRank": 2.5,
"mentionCount": 6,
"aiScore": 73,
"perProvider": {
"openai": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
},
"claude": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
},
"gemini": {
"rank": 2.3,
"mentioned": true,
"mentionCount": 3
}
},
"perPrompt": [
{
"promptIndex": 0,
"promptLabel": "best sales forecasting platforms for mid-market teams",
"mentionedBy": [
"openai",
"claude"
],
"score": 61
}
]
},
"topCompetitor": {
"domain": "competitor.com",
"name": "Competitor Inc",
"mentionRate": 88.9,
"avgRank": 1.8
},
"mentionRateGap": -22.2,
"totalCompetitorsFound": 8,
"totalQueries": 9,
"totalEntries": 45,
"competitorRankings": [
{
"domain": "competitor.com",
"name": "Competitor Inc",
"isOwn": false,
"openaiAvgRank": 2,
"claudeAvgRank": 1.7,
"geminiAvgRank": 3,
"overallAvgRank": 2.2,
"mentionCount": 7,
"mentionRate": 77.8,
"aiScore": 68,
"isTracked": false
}
]
},
"answers": [
{
"provider": "openai",
"promptIndex": 0,
"promptText": "best sales forecasting platforms for mid-market teams",
"brands": [
{
"rank": 1,
"name": "Competitor Inc",
"domain": "competitor.com",
"description": "Cloud platform for pipeline forecasting aimed at mid-market sales teams.",
"rankingRationale": "Listed first because it integrates directly with the major CRMs.",
"sentiment": "recommended",
"mentionContext": "direct_recommendation",
"targetAudience": "Mid-size B2B sales teams",
"pricingSignal": "mid_range",
"positionConfidence": 0.9,
"features": [
"pipeline forecasting",
"CRM sync"
],
"differentiation": {
"axis": "technology",
"uniqueValue": "Only platform with native two-way CRM sync"
},
"messaging": {
"keywords": [
"automation",
"mid-market",
"self-serve"
],
"credibilitySignals": [
"SOC 2 Type II",
"used by 400+ teams"
],
"differentiationClaims": [
"fastest setup in category"
]
}
}
]
}
],
"unansweredQueries": [
{
"provider": "gemini",
"promptIndex": 2,
"promptText": "best sales forecasting platforms for mid-market teams",
"reason": "no_usable_answer"
}
],
"providerStatus": {
"openai": {
"reported": true,
"completedAt": "2026-03-15T10:00:00.000Z",
"answersCounted": 3,
"brandsNamed": 21
},
"claude": {
"reported": true,
"completedAt": "2026-03-15T10:00:00.000Z",
"answersCounted": 3,
"brandsNamed": 21
},
"gemini": {
"reported": true,
"completedAt": "2026-03-15T10:00:00.000Z",
"answersCounted": 3,
"brandsNamed": 21
}
},
"answerCoverage": {
"queriesSent": 9,
"answersCounted": 8
},
"answersTruncated": false
}
}Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameters · invalid_run_id · invalid_check_id | Bad request — the payload failed validation. |
| 401 | api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scope | The CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope. |
| 404 | check_not_found | No check matches the id in the path. |
Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.
{
"error": {
"code": "invalid_parameters",
"message": "Domain is required",
"status": 400
}
}Get AI visibility trend data
/v1/projects/{projectId}/ai-visibility/trendGet 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.
Path parameters
| Name | Type | Description |
|---|---|---|
projectId | string | Project ID |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
dateFrom | string | Start date for trend data (ISO-8601) | |
dateTo | string | End date for trend data (ISO-8601) | |
provider | string | Filter by AI provider. Omit to get aggregated trend across all providers. Values: openai, claude, gemini. |
Request
curl "https://api.competlab.com/v1/projects/507f1f77bcf86cd799439011/ai-visibility/trend?dateFrom=2026-01-01&dateTo=2026-03-15&provider=openai" \
-H "CL-API-Key: YOUR_COMPETLAB_API_KEY"Response 200 OK
Example
{
"items": [
{
"date": "2026-03-15T10:00:00.000Z",
"customerMentionRate": 66.7,
"topCompetitorMentionRate": 88.9,
"topCompetitorDomain": "competitor.com",
"gap": -22.2,
"customerAiScore": 73,
"topCompetitorAiScore": 85
}
],
"incompleteCycles": [
{
"date": "2026-03-14T02:00:00.000Z",
"reason": "incomplete_coverage",
"measuredAnswers": 8,
"expectedAnswers": 9
}
]
}Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | api_key_missing · api_key_invalid · api_key_revoked · api_key_expired · insufficient_scope | The CL-API-Key is missing, malformed, revoked, expired, or lacks the required scope. |
Every error uses the shared { error: { code, message, status } } envelope; code is one of the values listed above.
{
"error": {
"code": "api_key_invalid",
"message": "Invalid API key",
"status": 401
}
}