Skip to main content
POST
Get Brand Intelligence Seo Group Insights Route

Authorizations

Authorization
string
header
required

Bearer authentication header of the form Bearer <token>, where <token> is your auth token.

Path Parameters

brand_id
string<uuid>
required

Body

application/json
domains
string[]
Maximum array length: 60
group_label
string | null
Maximum string length: 120

Response

Successful Response

brand_id
string<uuid>
required
generated_at
string<date-time>
required
group_key
string
required
module
string
default:seo
Allowed value: "seo"
group_label
string | null
brand_domain
string | null
domains
string[]
cohort_domains
string[]
coverage
ModuleCoverageStrip · object

N-of-M acquisition coverage for one module's cohort (the ★S0/★C0 strip).

measured counts cohort domains that produced a stored metric row. missing are targets that never did — their fetch or persist failed, or they have not been acquired yet. For Content, fetch_failed is a typed subset of missing, not an additional shortfall, so clients must de-duplicate rather than sum those lengths. A competitor with a failed acquisition must render as a failure chip, never as a measured zero.

position
ModuleDimensionRank · object[]
concentration
SeoVisibilityConcentration · object

★S2 — is the cohort's modelled organic traffic concentrated or spread.

top_domain is ONE name out of the domains holding the highest measured traffic; top_tied_count counts them inclusively (1 = it really is one domain). Without it, two domains at 50%/50% render as one dominating the cohort — the ★S2 twin of the tied_count disclosure on ModuleDimensionRank. None when nothing was measured, never 0.

table_stakes
SeoTableStakesPanel · object

★S3 — keywords the WHOLE measured cohort ranks for: the must-haves.

content_type_mix
SeoContentTypeMixPanel · object

★S6 — what KIND of page wins in this niche (classified ranking URLs).

Two classifiers feed one set of counts, and the split is published. Most labels come from the URL's own path (free, deterministic, and right whenever the path carries a recognisable token). ★S13b adds a second source for the paths that carry no such token: the page's actual parsed content, bought per URL. urls_labeled_from_content is how many labels came from the paid source across BOTH denominators — cohort_urls_classified plus brand_urls_classified, summed. The brand's own benchmark row spends its own budget exactly as each competitor row does, so a cohort-only count would understate the provenance and could not be rendered against either number alone.

That count is NOT a quality score and must not be rendered as one. It is bounded by a per-refresh budget spent only on URLs the free path could not read, so a LOW number means "the free classifier handled almost everything" — the good case — and is indistinguishable, by design, from "the paid leg is switched off". content_labeling_enabled is what separates them.

keyword_intent_mix
SeoKeywordIntentMixPanel · object

★S5 — classified intent mix over the stored cohort keyword samples.

keywords_unclassified is a separate denominator: missing labels are not folded into a bucket and domains whose classification call failed are named under domains_not_measured rather than represented as zeros.

authority
SeoAuthoritySpreadPanel · object

★S8 — backlink/DR distribution and where the brand sits.

enabled mirrors DATAFORSEO_BACKLINKS_ENABLED; when it is off the authority metrics were never bought, so the panel is not measured rather than a cohort of zeros.

authority_history
SeoAuthorityHistoryPanel · object

★S8 history — the authority side's first historical story.

enabled mirrors the acquisition gate (the link-history flag AND the backlinks transport switch); when it is off nothing was ever bought, so this is not measured rather than a cohort of flat zero lines.

★S9b — sites that link to the cohort and not to you.

The backlink twin of :class:SeoGapsPanel, and it carries the same three invariants. Rows come from the provider's own server-side exclusion query, so a site that links to the brand from a page we never sampled is still excluded. brand_referring_domains_known is the client-side belt's denominator: when it is 0 the belt was not available for this cohort and only the server-side exclusion applied. max_gap_population is a MAX across competitors, never a SUM — link-gap populations overlap heavily and adding them would invent a cohort-wide number nobody measured.

gaps
SeoGapsPanel · object

★S9 — the cohort ranks, you don't.

Rows come from DataForSEO's own server-side exclusion query (domain_intersection with intersections: false), so a keyword the brand ranks for at position 240 is not reported as a gap. semantics names that contract; an empty string means the stored snapshot predates it.

weaknesses
SeoWeaknessesPanel · object

★S10 — shared keywords where a competitor MATERIALLY outranks the brand.

The thresholds are the free SEO audit's, imported rather than copied, so BI and the audit can never disagree on what "materially outranks" means.

comparison_basis
SeoComparisonBasisPanel · object

Does a STORED predecessor exist for this cohort — at any age?

The SEO twin of :class:ContentComparisonBasisPanel and :class:GoogleAdsComparisonBasisPanel. Same two-field contract, same three states, over SEO's own grain and market invariant:

  • measured=False — no tracked domain has a stored capture at all, so we cannot say anything about history. An unknown, not a first generation.
  • measured=True and has_prior_snapshot=False — captures exist but none has a predecessor IN ITS OWN MARKET. This is the FIRST-EVER generation for the group, not "we compared and nothing changed".
  • has_prior_snapshot=True — a real market-matched comparison basis exists somewhere in the cohort.

Grain is the (domain, market) SERIES, not the bare domain. CompetitorSeoMetric carries no location_code/language_code of its own — both live on the parent CompetitorSeoSnapshot (app/models/competitor_dashboard.py) — so a predecessor pair must be scoped to the SAME market as the domain's current capture, never a different one. competitor_seo_dashboard_service.py::get_historical_traffic_series states this exact invariant in its own docstring: a brand's current-market live captures must never be joined to a different-location/language historical one. That is why the counts below are named series_with_* rather than domains_with_* (Google Ads' naming, which is legitimately domain-grain because paid-search captures carry no market split) — a domain that changed tracked market reads as TWO series here, and a stale cross-market pairing would silently diff unrelated numbers.

momentum
SeoDomainMovementRow · object[]
findings
ModuleRuleFinding · object[]
estimate_notice
string
default:""