curl --request GET \
--url https://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail', 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://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail",
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://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail"
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://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail")
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{
"competitor_domain": "<string>",
"basis": "this_workspace",
"market": {
"location_code": 123,
"language_code": "<string>",
"label": "<string>",
"country_code": "<string>",
"country_name": "<string>"
},
"authority_metrics_enabled": false,
"metric": {
"competitor_domain": "<string>",
"is_brand": false,
"organic_keyword_count": 123,
"keyword_sample_size": 123,
"est_organic_traffic": 123,
"est_traffic_value_usd": 123,
"domain_rank": 123,
"referring_domains": 123,
"backlinks": 123,
"keyword_gap_count": 123,
"keyword_gap_sample_size": 123,
"captured_at": "2023-11-07T05:31:56Z",
"previous_captured_at": "2023-11-07T05:31:56Z",
"previous_est_organic_traffic": 123,
"previous_organic_keyword_count": 123,
"previous_keyword_gap_count": 123,
"previous_counts_withheld": false,
"previous_organic_keyword_count_withheld": false,
"previous_keyword_gap_count_withheld": false
},
"keywords": [
{
"keyword": "<string>",
"position": 123,
"search_volume": 123,
"keyword_difficulty": 123,
"etv": 123,
"url": "<string>",
"rank_changes": {
"is_up": false,
"is_down": false,
"is_new": false,
"previous_rank": 123
},
"monthly_searches": [
{
"year": 123,
"month": 6,
"search_volume": 123
}
]
}
],
"traffic_series": {
"competitor_domain": "<string>",
"points": [
{
"captured_at": "2023-11-07T05:31:56Z",
"est_organic_traffic": 123,
"source": "live"
}
]
},
"gap_available": false,
"shared_keywords": [
{}
],
"gap_keywords": [
{}
],
"keyword_gap_sample_size": 123,
"gap_semantics": "",
"capture_count": 0
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}Get Competitor Seo Domain Detail
One domain’s whole organic-search record, for a caller that knows only the domain.
This is the read behind Brand Details’ SEO tab, which is reached from
Explore Brands by domain and has no roster context. It deliberately does
NOT require the domain to be one of brand_id’s tracked competitors:
organic keywords, traffic and authority are properties of the domain, so
requiring roster membership before reporting them refuses data the
platform holds. seo_svc.get_domain_seo_measurement prefers this
workspace’s own captures and falls back to any workspace’s, reporting
which in basis — and the workspace-relative keyword gap is served only
on the this_workspace branch (see that function and
CompetitorSeoDomainDetailResponse for why).
The ONE identity it does refuse is a domain on the operator blocklist — see the gate below for why that is not a contradiction of the paragraph above.
Scoped to the READING BRAND’s market on every branch, and market says
which. Roster context is optional here; market context is not — organic
numbers only mean something once a (location_code, language_code) is
fixed, so a domain measured in another market reads unmeasured rather
than lending its numbers to this one.
Read-only over data acquisition already bought: it never enqueues a refresh and never spends a vendor call, so opening the tab costs nothing.
curl --request GET \
--url https://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail', 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://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail",
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://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail"
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://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/brands/{brand_id}/dashboards/competitor-seo/domain-detail")
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{
"competitor_domain": "<string>",
"basis": "this_workspace",
"market": {
"location_code": 123,
"language_code": "<string>",
"label": "<string>",
"country_code": "<string>",
"country_name": "<string>"
},
"authority_metrics_enabled": false,
"metric": {
"competitor_domain": "<string>",
"is_brand": false,
"organic_keyword_count": 123,
"keyword_sample_size": 123,
"est_organic_traffic": 123,
"est_traffic_value_usd": 123,
"domain_rank": 123,
"referring_domains": 123,
"backlinks": 123,
"keyword_gap_count": 123,
"keyword_gap_sample_size": 123,
"captured_at": "2023-11-07T05:31:56Z",
"previous_captured_at": "2023-11-07T05:31:56Z",
"previous_est_organic_traffic": 123,
"previous_organic_keyword_count": 123,
"previous_keyword_gap_count": 123,
"previous_counts_withheld": false,
"previous_organic_keyword_count_withheld": false,
"previous_keyword_gap_count_withheld": false
},
"keywords": [
{
"keyword": "<string>",
"position": 123,
"search_volume": 123,
"keyword_difficulty": 123,
"etv": 123,
"url": "<string>",
"rank_changes": {
"is_up": false,
"is_down": false,
"is_new": false,
"previous_rank": 123
},
"monthly_searches": [
{
"year": 123,
"month": 6,
"search_volume": 123
}
]
}
],
"traffic_series": {
"competitor_domain": "<string>",
"points": [
{
"captured_at": "2023-11-07T05:31:56Z",
"est_organic_traffic": 123,
"source": "live"
}
]
},
"gap_available": false,
"shared_keywords": [
{}
],
"gap_keywords": [
{}
],
"keyword_gap_sample_size": 123,
"gap_semantics": "",
"capture_count": 0
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
Query Parameters
competitor domain
1Response
Successful Response
One domain's whole organic-search record, for a caller that knows only the domain.
Organic visibility is a property of a DOMAIN, so this response is keyed by
the requested domain rather than by the reading brand's competitor roster:
a domain the platform has measured for anyone reports real numbers here.
basis says whose acquisition paid for them and therefore what is in
the payload:
this_workspace— the reading brand's own captures. Thekeyword_gap*family is populated, because "ranks where the brand is absent" is measured against THIS brand's own website.platform— measured by another workspace. Only domain-intrinsic facts are carried;gap_keywords/shared_keywords/keyword_gap_countstay empty andgap_availableisFalse, because a foreign workspace's gap is measured against ITS website and is not this reader's to see.unmeasured— nobody has measured this domain.metricis null and every collection is empty. Render that as "not measured", never as zero (ADR 0026): every numeric here is nullable and null means NOT MEASURED.
this_workspace, platform, unmeasured WHICH market an organic-search series describes.
Organic rank, traffic and keyword counts are measured per
(location_code, language_code): the same domain measured in the US and
in the UK produces two unrelated numbers. Every SEO reader here is scoped
to exactly one market (see get_traffic_series /
get_historical_traffic_series / get_domain_seo_measurement), and a
brand can change its market at any time from the market modal — so a
series that does not NAME its market silently changes basis under the
reader, who has no way to tell that the axis moved.
Carried alongside basis and for the same reason: state what was
measured rather than render a plausible number. basis names WHOSE
acquisition paid; this names WHAT MARKET it bought.
country_code / country_name are null for a location_code
outside brand_market_service.MARKET_COUNTRIES — a market can be
persisted (or configured as the fallback default) without appearing in the
picker's catalogue. label is always populated and always renderable:
it degrades to the bare code rather than to an empty string, because the
whole point of this object is that the reader is never left guessing.
Show child attributes
Show child attributes
One competitor's organic row.
Every numeric here is nullable and null means NOT MEASURED, never zero. A DataForSEO read that failed persists NULL so the UI renders its NOT_MEASURED em-dash instead of an authoritative-looking 0.
organic_keyword_count / keyword_gap_count are DOMAIN-WIDE
populations. keyword_sample_size / keyword_gap_sample_size are how
many rows the Keywords and Gap panels actually hold — so the UI can label
them "top N of M" rather than presenting a capped API page as the count.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Was this page helpful?