Get Brand Intelligence Content Group Insights Route
Cohort content analysis for one competitor Brand Group (INSIGHTS tab).
Includes the content ↔ demand join, which reads the SAME cohort’s stored SEO keyword universe. No new provider spend on either side.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Path Parameters
Body
Content cohort plus the vocabulary used by the pillar roll-up.
auto derives a deterministic starting vocabulary from the stored
cohort evidence and ignores rollup_pillars. frozen uses exactly
the supplied vocabulary. Carrying that vocabulary in the request keeps a
saved comparison stable when later acquisitions discover new terms.
Response
Successful Response
"content"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.
★C0 — per-competitor source health rolled up for the cohort.
Failed sources used to be DELETED from the UI, which made a brand whose blog was blocked look identical to one with no blog.
★C1 — publishing velocity vs measured competitors.
measured_count and cohort_median_posts exclude the user's brand;
brand_rank ranks the brand across the combined measured population.
brand_rank is a COMPETITION rank ("1224"): domains that published the
same number of posts share a place, and only a domain that published MORE
pushes the brand down one. So rank 1 means "nobody published more" — NOT
"published more than everyone", which a tie also satisfies.
brand_tied_count includes the brand itself and makes that shared place
explicit to every renderer.
rows_with_suppressed_cadence counts over the FULL cohort, not the
truncated rows list, and is None rather than 0 when nothing was
suppressed: module_group_commentary._flatten_refs skips None but
flattens 0 into evidence the narrator may quote, so a 0 would hand the
model a "0 rows suppressed" fact about every cohort.
★C2 — article/video/gated split across the cohort vs the brand.
★C3 — shared topics (table stakes) vs uncontested ground.
Request-scoped pillar vocabulary rolled across stored item samples.
Unlike the sibling full-cohort panels, this comparison deliberately caps competitors before vocabulary and aggregate computation. The explicit total/included/truncated accounting prevents that bounded denominator from masquerading as complete cohort coverage.
★C10 — the shape of the cohort's headlines, from stored titles only.
Fully deterministic: every field is arithmetic over
CompetitorContentMetric.latest_items[*].title, which is already
persisted. No provider call, no model call, no acquisition — see
content_headline_patterns.py for why an LLM was declined.
Four states, deliberately distinguishable:
measured=False+not_measured_reason="no_content_measured";measured=False+"no_titles_stored"(read, but zero titles);measured=False+"insufficient_titles"(below the floor — the counts are still carried so the reader sees "12 of 20 needed");measured=Truewith a0.0share — a measured zero.
A measured panel carries a row for EVERY pattern, including unused ones. Filtering the zeros would make a real "nobody does this" indistinguishable from "we did not check".
★C4 — the cohort's tried-and-true capture patterns.
★C8 — the content ↔ demand join, benchmarked on the SAME cohort.
Three buckets: pillars the brand publishes that the cohort's SEO keyword universe confirms have demand; demand the cohort captures that the brand publishes nothing for; and pillars the brand publishes with no demand evidence anywhere in the cohort's keyword universe.
measured is False unless BOTH sides were measured for this cohort —
a missing SEO capture must never render as "no demand".
cohort_demand is the brand-side-WITHHELD half of the join: the demand
the cohort captures, with brand_publishes=None on every row. It is
populated ONLY when the SEO side is measured and the brand side is not,
which is the default new-cohort state — a fully-measured cohort should not
render as a blank panel just because the brand has not been read yet. When
measured is True this list is empty and the three comparison buckets
carry the answer, so no term is ever rendered twice.
★C8b — the content ↔ paid-ads join, benchmarked on the SAME cohort.
Three buckets: pillars the brand publishes that the cohort's Google Ads keyword universe confirms someone is backing with paid spend; paid keywords the cohort bids on that the brand publishes no content for; and pillars the brand publishes with no paid-keyword evidence anywhere in the cohort's Ads universe.
measured is False unless BOTH sides were measured for this cohort —
a missing Ads capture must never render as "no paid demand".
cohort_ad_spend is the brand-side-WITHHELD half of the join: the paid
keywords the cohort backs, with brand_publishes=None on every row. It
is populated ONLY when the ads side is measured and the brand side is
not, which is the default new-cohort state — a fully-measured cohort
should not render as a blank panel just because the brand has not been
read yet. When measured is True this list is empty and the three
comparison buckets carry the answer, so no term is ever rendered twice.
Weekly observed AI-answer mentions and citations for this cohort.
This is a read-only projection of the already-bounded weekly SEO capture; opening Content Insights never triggers a provider request.
Content's first MEASURED performance signal (design 9.2).
Every other Content panel counts what a publisher SHIPPED. This one reports what individual pieces EARNED, joined by URL onto the same bounded item sample the rest of the module already renders.
Three states, and collapsing any two is the defect this panel exists to avoid:
measured=False- no publisher in this cohort has a stored page-index capture. Nothing can be said. This is the state a cohort sits in while the acquisition flag is off, and it must never render as zeros.measured=Truewithrows == []- a page index WAS read for at least one publisher and none of the sampled URLs appear in it. A real finding, sized bypages_sampled, not a failure.measured=Truewith rows - render them, remembering that a blank cell inside a row is still "not measured".
Which of a publisher's PAGES an AI assistant cites (design 9.3).
Deliberately its own panel rather than three more columns on
ContentPagePerformancePanel, and deliberately NOT merged into the
module's brand-level AI visibility panel. The three are different claims:
- brand-level AI visibility (C5) - is this BRAND named when an assistant is
asked about the category. Re-read of the SEO module's
llm_scrapercapture. - per-page citations (this) - is this URL cited. DataForSEO LLM Mentions, keyed per page, different denominator.
- per-page organic/link performance - did this URL earn search traffic and links. No AI surface involved at all.
A reader must be able to tell which is which, so each gets its own heading,
its own basis notice and its own measured/unmeasured verdict. platform
is required reading alongside any count: "cited by ChatGPT" and "cited in
Google's AI Overviews" are not interchangeable.
What the tracked publishers BUILD with, versus what the brand builds with.
Every other Content panel measures output. This one measures the content stack behind the output — CMS, static-site generator, analytics, tag manager, personalisation, email platform — so a reader can see that four of the publishers they track run a headless CMS and they do not.
The claim is bounded on three axes, and each one is on the payload.
- Evidence direction. Only detections are counted.
detected_countis out ofcompetitors_scanned, never out ofcompetitors_in_cohort: a domain nobody scanned is not a domain that lacks the technology. - Sample size.
competitors_scanned/competitors_in_cohortsizes the comparison, andbrand_scannedsays whether the "versus you" half is expressible at all. When it is False the differential is withheld — enforced below, not merely documented. - Age. Detections carry the provider's own
last_visited, which can be materially older than the refresh that fetched it.oldest_detected_atbounds how current the comparison is andstale_domain_countsays how much of it is paststale_after_days.
Three states, as everywhere else in this module: measured=False is "no
cohort domain has a stored scan" and must never render as "no technologies";
measured=True with rows == [] is "we scanned and detected nothing we
report", which is genuinely rare and still not proof of an empty stack.
25Whether a period-over-period comparison is possible for this cohort AT ALL.
Three states. Collapsing any two of them is the defect this panel exists to prevent, because each one licenses a different sentence on screen:
measured=False— no tracked publisher has a stored capture, so we cannot say anything about history. Not a first generation; an unknown.measured=Trueandhas_prior_snapshot=False— captures exist but none has a predecessor on its own measurement series. This is the FIRST-EVER generation for the group: there is nothing to compare against, which is emphatically not "we compared and nothing changed".has_prior_snapshot=True— a real comparison basis exists, so an empty change feed is a measured absence of qualifying movement.
The series grain matches content_insight_events's own baseline test
(detect_content_publisher_delta returns None when the previous
snapshot on the same domain/source/source_url series is
missing): a publisher needs two captures on ONE series before any delta
can exist. Normalization here is deliberately no tighter than the emitter's
so that a mismatch can only over-count history — which suppresses the
first-generation copy and falls back to the existing wording, never the
other way round.