curl --request GET \
--url https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"start_date": "2023-12-25",
"end_date": "2023-12-25",
"comparison_start_date": "2023-12-25",
"comparison_end_date": "2023-12-25",
"before_visibility": 123,
"after_visibility": 123,
"before_responses": 123,
"after_responses": 123,
"dimensions": [
{
"dimension": "service",
"segments": [
{
"id": "<string>",
"name": "<string>",
"metadata": {},
"before_visibility": 123,
"after_visibility": 123,
"before_responses": 123,
"after_responses": 123,
"before_mentions": 123,
"after_mentions": 123,
"contribution": 123,
"within_noise": true
}
],
"mix_contribution": 123
}
]
}{
"message": "<string>",
"errors": {}
}Visibility Change Decomposition
Explains why the brand’s visibility score changed between the selected period and the previous period of equal length. The comparison window is derived automatically and echoed back as comparison_start_date / comparison_end_date.
The change is decomposed across three dimensions - services, countries and topics - each of which partitions the analysed response set. Within a dimension, every segment carries a contribution: the visibility-point effect (on the 0-1 scale) of that segment’s own mention-rate movement, weighted by the segment’s share of responses. The dimension’s mix_contribution captures change caused by a shift in where responses were collected (added platforms, new prompts, different response volumes) rather than by mention-rate movement. For every dimension, the segment contributions plus mix_contribution sum exactly to after_visibility - before_visibility.
within_noise: true marks segments whose sample is too small (fewer than 30 responses in either period) or whose movement sits inside the 95% confidence interval for the rate difference; treat those movements as inconclusive.
Segment ids use the same public identifiers as the filter parameters (service providers, country codes, topic UUIDs or no_topic), so drilling down is iterative: pass a segment’s id back as the matching filter (services, countries, topics) and call the endpoint again to decompose that slice across the remaining dimensions. Use GET /api/v1/projects/{project}/mentions with the same filters to retrieve concrete example responses.
When every partitioning dimension has been pinned to a single segment, pass include_prompts=true to reach the final level: the individual prompts that moved the slice.
curl --request GET \
--url https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.surfacd.com/api/v1/projects/{project}/visibility-decomposition")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"start_date": "2023-12-25",
"end_date": "2023-12-25",
"comparison_start_date": "2023-12-25",
"comparison_end_date": "2023-12-25",
"before_visibility": 123,
"after_visibility": 123,
"before_responses": 123,
"after_responses": 123,
"dimensions": [
{
"dimension": "service",
"segments": [
{
"id": "<string>",
"name": "<string>",
"metadata": {},
"before_visibility": 123,
"after_visibility": 123,
"before_responses": 123,
"after_responses": 123,
"before_mentions": 123,
"after_mentions": 123,
"contribution": 123,
"within_noise": true
}
],
"mix_contribution": 123
}
]
}{
"message": "<string>",
"errors": {}
}Authorizations
Team API key created in Team Settings. Use it with URLs for the same team.
Path Parameters
Project UUID.
Query Parameters
Start of the current period. The previous period of equal length, ending the day before start_date, is compared automatically.
End of the current period. Must be on or after start_date.
Brand UUID to analyse. Must belong to the project. Defaults to the project's primary brand. Discover brand UUIDs with GET /api/v1/projects/{project}/brands.
AI platform provider filters such as chatgpt or perplexity. Pin a single provider (using a segment id from a previous response) to drill into that platform. Discover providers with GET /api/v1/platforms.
Country code filters such as US or GB. Pin a single code (using a segment id from a previous response) to drill into that country. Discover codes with GET /api/v1/countries.
Topic UUID or name filters. Use no_topic for prompts without a topic. Pin a single topic (using a segment id from a previous response) to drill into it. Discover topics with GET /api/v1/projects/{project}/topics.
Tag UUID or name filters. Tags restrict the analysed prompt set but are not a drill dimension. Discover tags with GET /api/v1/projects/{project}/tags.
Prompt UUID filter.
When true, appends a prompt dimension listing the individual prompts with the largest absolute contributions (capped at 10, ranked largest first). Use it at the end of a drill to see which questions drove the change. Prompt segment ids are prompt UUIDs and can be passed back as the prompt filter or to the mentions endpoint. Unlike the partitioning dimensions, this capped list is a ranked shortlist - its contributions do not necessarily sum to the overall change.
Response
Period-over-period visibility change decomposed per dimension.
Start of the automatically derived previous period.
End of the automatically derived previous period.
Overall visibility in the comparison period (0-1 scale).
Overall visibility in the current period (0-1 scale).
Show child attributes
Show child attributes