> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aitasker.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Get Brand Intelligence Google Ads Group Insights Route

> Cohort paid-search analysis for one competitor Brand Group (INSIGHTS tab).

POST because the cohort is a client-curated domain set, mirroring the SEO
route. An empty ``domains`` list means "the whole tracked set". Reads only
already-persisted DataForSEO captures — no new provider spend.



## OpenAPI

````yaml /openapi.json post /api/v1/brands/{brand_id}/brand-intelligence/google-ads-group-insights
openapi: 3.1.0
info:
  title: AITasker
  version: 0.1.0
servers: []
security: []
paths:
  /api/v1/brands/{brand_id}/brand-intelligence/google-ads-group-insights:
    post:
      tags:
        - Brand Intelligence
        - Brand Intelligence
      summary: Get Brand Intelligence Google Ads Group Insights Route
      description: >-
        Cohort paid-search analysis for one competitor Brand Group (INSIGHTS
        tab).


        POST because the cohort is a client-curated domain set, mirroring the
        SEO

        route. An empty ``domains`` list means "the whole tracked set". Reads
        only

        already-persisted DataForSEO captures — no new provider spend.
      operationId: >-
        get_brand_intelligence_google_ads_group_insights_route_api_v1_brands__brand_id__brand_intelligence_google_ads_group_insights_post
      parameters:
        - name: brand_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
            title: Brand Id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GoogleAdsGroupInsightsRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GoogleAdsGroupInsightsResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      security:
        - HTTPBearer: []
components:
  schemas:
    GoogleAdsGroupInsightsRequest:
      properties:
        domains:
          items:
            type: string
          type: array
          maxItems: 60
          title: Domains
        group_label:
          anyOf:
            - type: string
              maxLength: 120
            - type: 'null'
          title: Group Label
      type: object
      title: GoogleAdsGroupInsightsRequest
    GoogleAdsGroupInsightsResponse:
      properties:
        module:
          type: string
          const: google-ads
          title: Module
          default: google-ads
        brand_id:
          type: string
          format: uuid
          title: Brand Id
        generated_at:
          type: string
          format: date-time
          title: Generated At
        group_key:
          type: string
          title: Group Key
        group_label:
          anyOf:
            - type: string
            - type: 'null'
          title: Group Label
        brand_domain:
          anyOf:
            - type: string
            - type: 'null'
          title: Brand Domain
        domains:
          items:
            type: string
          type: array
          title: Domains
        cohort_domains:
          items:
            type: string
          type: array
          title: Cohort Domains
        coverage:
          $ref: '#/components/schemas/ModuleCoverageStrip'
        totals:
          $ref: '#/components/schemas/GoogleAdsGroupTotals'
        members:
          items:
            $ref: '#/components/schemas/GoogleAdsMemberRow'
          type: array
          title: Members
        brand_reference:
          anyOf:
            - $ref: '#/components/schemas/GoogleAdsMemberRow'
            - type: 'null'
        momentum:
          $ref: '#/components/schemas/GoogleAdsMomentumPanel'
        gap_consensus:
          $ref: '#/components/schemas/GoogleAdsGapConsensusPanel'
        overlap_pressure:
          $ref: '#/components/schemas/GoogleAdsOverlapPanel'
        ad_copy:
          $ref: '#/components/schemas/GoogleAdsAdCopyPanel'
        findings:
          items:
            $ref: '#/components/schemas/ModuleRuleFinding'
          type: array
          title: Findings
        estimate_notice:
          type: string
          title: Estimate Notice
          default: >-
            Estimated — modelled from competitor paid keywords (DataForSEO). Not
            actual competitor spend.
      type: object
      required:
        - brand_id
        - generated_at
        - group_key
      title: GoogleAdsGroupInsightsResponse
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    ModuleCoverageStrip:
      properties:
        targets_total:
          type: integer
          title: Targets Total
          default: 0
        measured:
          type: integer
          title: Measured
          default: 0
        missing:
          items:
            type: string
          type: array
          title: Missing
        fetch_failed:
          items:
            type: string
          type: array
          title: Fetch Failed
        persist_failed:
          items:
            type: string
          type: array
          title: Persist Failed
        skipped_cost:
          items:
            type: string
          type: array
          title: Skipped Cost
        last_run_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Last Run At
        brand_measured:
          type: boolean
          title: Brand Measured
          default: false
        brand_domain:
          anyOf:
            - type: string
            - type: 'null'
          title: Brand Domain
        brand_not_measured_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Brand Not Measured Reason
        near_duplicate_pairs:
          items:
            $ref: '#/components/schemas/ModuleCoverageNearDuplicatePair'
          type: array
          title: Near Duplicate Pairs
        near_duplicate_scan_truncated:
          type: boolean
          title: Near Duplicate Scan Truncated
          default: false
        near_duplicate_scanned_count:
          type: integer
          title: Near Duplicate Scanned Count
          default: 0
      type: object
      title: ModuleCoverageStrip
      description: >-
        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.
    GoogleAdsGroupTotals:
      properties:
        measured_count:
          type: integer
          title: Measured Count
          default: 0
        est_group_monthly_spend_usd:
          anyOf:
            - type: number
            - type: 'null'
          title: Est Group Monthly Spend Usd
        spend_measured_count:
          type: integer
          title: Spend Measured Count
          default: 0
        est_group_paid_traffic:
          anyOf:
            - type: number
            - type: 'null'
          title: Est Group Paid Traffic
        traffic_measured_count:
          type: integer
          title: Traffic Measured Count
          default: 0
        median_cpc_usd:
          anyOf:
            - type: number
            - type: 'null'
          title: Median Cpc Usd
        cpc_measured_count:
          type: integer
          title: Cpc Measured Count
          default: 0
        data_from:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Data From
        data_to:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Data To
        estimate_notice:
          type: string
          title: Estimate Notice
          default: >-
            Estimated — modelled from competitor paid keywords (DataForSEO). Not
            actual competitor spend.
        sample_notice:
          type: string
          title: Sample Notice
          default: >-
            Keyword counts are the stored tracked sample (up to 200 per brand),
            not the full paid-keyword population.
      type: object
      title: GoogleAdsGroupTotals
      description: |-
        Header-tile aggregates over MEASURED members only.

        Every dollar/traffic/CPC figure is a DataForSEO MODEL — the D3 estimate
        notice ships with the payload so no renderer can forget it. Each metric
        carries its own measured-member count: a sum over 4 of 10 members must
        say so.
    GoogleAdsMemberRow:
      properties:
        domain:
          type: string
          title: Domain
        est_monthly_spend_usd:
          anyOf:
            - type: number
            - type: 'null'
          title: Est Monthly Spend Usd
        spend_share_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Spend Share Pct
        est_paid_traffic:
          anyOf:
            - type: number
            - type: 'null'
          title: Est Paid Traffic
        paid_keyword_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Paid Keyword Count
        avg_cpc_usd:
          anyOf:
            - type: number
            - type: 'null'
          title: Avg Cpc Usd
        paid_vs_organic_ratio:
          anyOf:
            - type: number
            - type: 'null'
          title: Paid Vs Organic Ratio
        overlap_keyword_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Overlap Keyword Count
        gap_keyword_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Gap Keyword Count
        captured_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Captured At
      type: object
      required:
        - domain
      title: GoogleAdsMemberRow
      description: |-
        One measured member — feeds the leaderboard, positioning map and
        CPC/paid-dependence ladder from a single fetch.
    GoogleAdsMomentumPanel:
      properties:
        window_days:
          type: integer
          title: Window Days
          default: 90
        series:
          items:
            $ref: '#/components/schemas/GoogleAdsSpendSeriesEntry'
          type: array
          title: Series
        risers:
          items:
            $ref: '#/components/schemas/GoogleAdsMover'
          type: array
          title: Risers
        faders:
          items:
            $ref: '#/components/schemas/GoogleAdsMover'
          type: array
          title: Faders
        keyword_risers:
          items:
            $ref: '#/components/schemas/GoogleAdsKeywordMover'
          type: array
          title: Keyword Risers
        keyword_faders:
          items:
            $ref: '#/components/schemas/GoogleAdsKeywordMover'
          type: array
          title: Keyword Faders
        insufficient_history:
          items:
            type: string
          type: array
          title: Insufficient History
        measured:
          type: boolean
          title: Measured
          default: false
        comparison_basis:
          $ref: '#/components/schemas/GoogleAdsComparisonBasisPanel'
        estimate_notice:
          type: string
          title: Estimate Notice
          default: >-
            Estimated — modelled from competitor paid keywords (DataForSEO). Not
            actual competitor spend.
      type: object
      title: GoogleAdsMomentumPanel
      description: |-
        Windowed spend movement. History is event-driven (bootstrap + manual
        refresh), so sparse series are the EXPECTED state — members with fewer
        than two in-window measured points land in ``insufficient_history``,
        never as a zero-change mover.
    GoogleAdsGapConsensusPanel:
      properties:
        keywords:
          items:
            $ref: '#/components/schemas/GoogleAdsGapKeyword'
          type: array
          title: Keywords
        measured_domains:
          type: integer
          title: Measured Domains
          default: 0
        union_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Union Count
        consensus_count:
          type: integer
          title: Consensus Count
          default: 0
        total_search_volume:
          anyOf:
            - type: integer
            - type: 'null'
          title: Total Search Volume
        measured:
          type: boolean
          title: Measured
          default: false
        promoted_keyword:
          anyOf:
            - type: string
            - type: 'null'
          title: Promoted Keyword
        displaced_keyword:
          anyOf:
            - type: string
            - type: 'null'
          title: Displaced Keyword
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            Keyword counts are the stored tracked sample (up to 200 per brand),
            not the full paid-keyword population.
      type: object
      title: GoogleAdsGapConsensusPanel
      description: >-
        Keywords to steal — the cross-member union of stored gap lists.


        ``union_count`` is a LOWER BOUND (each member's list is a <=200-row

        sample); ``consensus_count`` counts keywords >=2 members bid on, over
        the

        FULL union, never the rendered slice. ``promoted_keyword`` /

        ``displaced_keyword`` disclose the last-row substitution; both None
        means

        the rendered slice is exactly the top-ranked rows.
    GoogleAdsOverlapPanel:
      properties:
        rows:
          items:
            $ref: '#/components/schemas/GoogleAdsContestedMemberRow'
          type: array
          title: Rows
        measured_domains:
          type: integer
          title: Measured Domains
          default: 0
        measured:
          type: boolean
          title: Measured
          default: false
        position_basis:
          type: string
          title: Position Basis
          default: >-
            SERP position proxy from stored paid-intersection rows — not Google
            Auction Insights.
      type: object
      title: GoogleAdsOverlapPanel
    GoogleAdsAdCopyPanel:
      properties:
        members:
          items:
            $ref: '#/components/schemas/GoogleAdsMemberCreatives'
          type: array
          title: Members
        measured_domains:
          type: integer
          title: Measured Domains
          default: 0
        total_creatives:
          anyOf:
            - type: integer
            - type: 'null'
          title: Total Creatives
        text_count:
          type: integer
          title: Text Count
          default: 0
        image_count:
          type: integer
          title: Image Count
          default: 0
        video_count:
          type: integer
          title: Video Count
          default: 0
        top_ctas:
          items:
            $ref: '#/components/schemas/GoogleAdsCtaRankRow'
          type: array
          title: Top Ctas
        longest_running:
          items:
            $ref: '#/components/schemas/GoogleAdsLongestRunningRow'
          type: array
          title: Longest Running
        changes:
          $ref: '#/components/schemas/GoogleAdsCreativeChanges'
        measured:
          type: boolean
          title: Measured
          default: false
        capture_notice:
          type: string
          title: Capture Notice
          default: >-
            Captured sample from the Google Ads Transparency Center — not a
            census of the group's ads.
      type: object
      title: GoogleAdsAdCopyPanel
      description: |-
        What the group says in its ads — captured creatives per member, exact
        CTA frequency (deterministic, no LLM in the measured layer), and the
        observed-run-time hint for creatives that carried both dates.
    ModuleRuleFinding:
      properties:
        rule_id:
          type: string
          title: Rule Id
        module:
          type: string
          enum:
            - seo
            - content
            - reviews
            - social
            - google-ads
            - audits
          title: Module
        panel:
          type: string
          title: Panel
        severity:
          type: string
          enum:
            - info
            - opportunity
            - risk
          title: Severity
          default: info
        title:
          type: string
          title: Title
        detail:
          type: string
          title: Detail
        evidence_refs:
          items:
            type: string
          type: array
          title: Evidence Refs
      type: object
      required:
        - rule_id
        - module
        - panel
        - title
        - detail
      title: ModuleRuleFinding
      description: |-
        One deterministic finding emitted by the rule engine.

        Every LLM recommendation must cite one of these ``rule_id``s, so the
        narrative can neither contradict nor exceed the measured layer.
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError
    ModuleCoverageNearDuplicatePair:
      properties:
        domain_a:
          type: string
          title: Domain A
        domain_b:
          type: string
          title: Domain B
        edit_distance:
          type: integer
          title: Edit Distance
      type: object
      required:
        - domain_a
        - domain_b
        - edit_distance
      title: ModuleCoverageNearDuplicatePair
      description: |-
        One "these might be the same brand" hint (audit Tier 0.3).

        Two cohort domains within ``edit_distance`` of each other on their
        REGISTRABLE (eTLD+1) form — e.g. ``acme.com``/``acm3.com`` (a typo), or
        ``blog.acme.com``/``shop.acme.com`` (two subdomains of the same apex,
        both normalizing to ``acme.com``, distance 0). This is what closed the
        ``warbybarker.com`` vs ``warbyparker.com`` gap the audit's D5 finding
        named (distance 1, exact-string dedup never caught it).

        **Advisory only.** The audit's own phrasing is "surface, don't
        auto-merge" — no code path may read this field to combine, drop, or
        otherwise alter which domains belong to a cohort. It exists purely so a
        coverage strip can ask the operator/user "are these two the same
        brand?" instead of silently double-counting or silently guessing.
    GoogleAdsSpendSeriesEntry:
      properties:
        domain:
          type: string
          title: Domain
        points:
          items:
            $ref: '#/components/schemas/GoogleAdsSpendSeriesPoint'
          type: array
          title: Points
      type: object
      required:
        - domain
      title: GoogleAdsSpendSeriesEntry
    GoogleAdsMover:
      properties:
        domain:
          type: string
          title: Domain
        delta_usd:
          type: number
          title: Delta Usd
        delta_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Delta Pct
        from_captured_at:
          type: string
          format: date-time
          title: From Captured At
        to_captured_at:
          type: string
          format: date-time
          title: To Captured At
      type: object
      required:
        - domain
        - delta_usd
        - from_captured_at
        - to_captured_at
      title: GoogleAdsMover
      description: >-
        Equal-basis windowed spend delta — BOTH endpoints measured in-window,

        and the compared capture dates ride the payload so the UI can stamp
        them.
    GoogleAdsKeywordMover:
      properties:
        domain:
          type: string
          title: Domain
        delta_keywords:
          type: integer
          title: Delta Keywords
        delta_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Delta Pct
        from_captured_at:
          type: string
          format: date-time
          title: From Captured At
        to_captured_at:
          type: string
          format: date-time
          title: To Captured At
      type: object
      required:
        - domain
        - delta_keywords
        - from_captured_at
        - to_captured_at
      title: GoogleAdsKeywordMover
      description: Equal-basis windowed change in the SAMPLED paid-keyword count.
    GoogleAdsComparisonBasisPanel:
      properties:
        measured:
          type: boolean
          title: Measured
          default: false
        domains_with_captures:
          type: integer
          title: Domains With Captures
          default: 0
        domains_with_prior_capture:
          type: integer
          title: Domains With Prior Capture
          default: 0
        has_prior_snapshot:
          type: boolean
          title: Has Prior Snapshot
          default: false
        domains_with_in_window_prior_capture:
          type: integer
          title: Domains With In Window Prior Capture
          default: 0
      type: object
      title: GoogleAdsComparisonBasisPanel
      description: >-
        Does a STORED predecessor exist for this cohort — at any age?


        The paid-search twin of :class:`ContentComparisonBasisPanel`, and the
        two

        shared flags answer the SAME question in both modules, over the module's

        whole stored history:


        - ``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
          no domain has a second one, at any age. This is the FIRST-EVER
          generation for the group: there is genuinely nothing to compare, which
          is not "we looked and could not measure it".
        - ``has_prior_snapshot=True`` — a stored predecessor exists somewhere in
          the cohort's history. It may still be older than the momentum window,
          in which case the panel stays unmeasured — but the group is NOT on its
          first capture, and must not be told that it is.

        Why unbounded, when the panel it hangs off is windowed. The notice these

        flags drive says "this is the first stored capture", which is a claim

        about the cohort's HISTORY, not about the last 90 days. Deriving it from

        the windowed ``series`` made a domain with a capture older than the
        window

        plus one inside it read as a first generation — a sentence a reader who

        opened the stored data could immediately falsify. The window-scoped fact

        keeps its own explicitly window-named field below, and

        ``GoogleAdsMomentumPanel.measured`` still reads THAT one.


        ``measured`` / ``has_prior_snapshot`` are the two-field contract the

        frontend's ``comparisonBasisNotice()`` reads; both panels expose them

        under those exact names AND with the same meaning, and

        ``tests/contracts/shapes/test_module_comparison_basis_contract.py`` pins

        both halves. The COUNTS are deliberately module-specific in name and

        grain: Content counts publishers over ``(domain, source, source_url)``

        measurement series, this counts competitor domains. Naming them alike

        would imply a shared denominator they do not have.
    GoogleAdsGapKeyword:
      properties:
        keyword:
          type: string
          title: Keyword
        member_domains:
          type: integer
          title: Member Domains
          default: 0
        example_domains:
          items:
            type: string
          type: array
          title: Example Domains
        search_volume:
          anyOf:
            - type: integer
            - type: 'null'
          title: Search Volume
        cpc:
          anyOf:
            - type: number
            - type: 'null'
          title: Cpc
        best_position:
          anyOf:
            - type: integer
            - type: 'null'
          title: Best Position
      type: object
      required:
        - keyword
      title: GoogleAdsGapKeyword
      description: |-
        One keyword members bid on that the brand does not (gap rows are
        brand-relative by the fixed target1=brand contract).
    GoogleAdsContestedMemberRow:
      properties:
        domain:
          type: string
          title: Domain
        shared_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Shared Count
        comparable_count:
          type: integer
          title: Comparable Count
          default: 0
        outranked_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Outranked Count
        outranked_share_pct:
          anyOf:
            - type: number
            - type: 'null'
          title: Outranked Share Pct
        top_contested:
          items:
            $ref: '#/components/schemas/GoogleAdsContestedKeyword'
          type: array
          title: Top Contested
      type: object
      required:
        - domain
      title: GoogleAdsContestedMemberRow
      description: |-
        One member's overlap pressure vs the brand's own domain.

        ``shared_count`` None = overlap never measured for this member (no
        brand_domain at fetch time) — never rendered as zero. ``outranked_*`` is
        computed only over rows where BOTH positions were observed
        (``comparable_count``).
    GoogleAdsMemberCreatives:
      properties:
        domain:
          type: string
          title: Domain
        text_count:
          type: integer
          title: Text Count
          default: 0
        image_count:
          type: integer
          title: Image Count
          default: 0
        video_count:
          type: integer
          title: Video Count
          default: 0
        creatives:
          items:
            $ref: '#/components/schemas/GoogleAdsCreativeItem'
          type: array
          title: Creatives
      type: object
      required:
        - domain
      title: GoogleAdsMemberCreatives
    GoogleAdsCtaRankRow:
      properties:
        cta:
          type: string
          title: Cta
        count:
          type: integer
          title: Count
          default: 0
        member_domains:
          type: integer
          title: Member Domains
          default: 0
      type: object
      required:
        - cta
      title: GoogleAdsCtaRankRow
    GoogleAdsLongestRunningRow:
      properties:
        domain:
          type: string
          title: Domain
        headline:
          anyOf:
            - type: string
            - type: 'null'
          title: Headline
        observed_running_days:
          type: integer
          title: Observed Running Days
          default: 0
        started_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Started At
      type: object
      required:
        - domain
      title: GoogleAdsLongestRunningRow
    GoogleAdsCreativeChanges:
      properties:
        measured:
          type: boolean
          title: Measured
          default: false
        new_count:
          type: integer
          title: New Count
          default: 0
        retired_count:
          type: integer
          title: Retired Count
          default: 0
        compared_domains:
          type: integer
          title: Compared Domains
          default: 0
        previous_captured_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Previous Captured At
        latest_captured_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Latest Captured At
        basis_notice:
          type: string
          title: Basis Notice
          default: >-
            Compared against each brand's previous capture, over creatives the
            provider gave a stable id. Unkeyed creatives cannot be matched
            across captures and are counted as neither new nor retired.
      type: object
      title: GoogleAdsCreativeChanges
      description: >-
        New / retired creatives between each member's last two captures.


        NOT a fixed calendar period: captures are irregular, so "this period" is

        the interval between a brand's own previous capture and its latest one.

        Stating the two timestamps is what keeps that honest — a reader can see

        the window rather than assume a week.


        ``measured`` is False when no member has a previous capture to compare,

        which is the ordinary state for a newly tracked cohort. That is a

        NOT-MEASURED state, never "nothing changed" — the module's standing
        rule.
    GoogleAdsSpendSeriesPoint:
      properties:
        captured_at:
          type: string
          format: date-time
          title: Captured At
        est_monthly_spend_usd:
          anyOf:
            - type: number
            - type: 'null'
          title: Est Monthly Spend Usd
        paid_keyword_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Paid Keyword Count
      type: object
      required:
        - captured_at
      title: GoogleAdsSpendSeriesPoint
    GoogleAdsContestedKeyword:
      properties:
        keyword:
          type: string
          title: Keyword
        search_volume:
          anyOf:
            - type: integer
            - type: 'null'
          title: Search Volume
        cpc:
          anyOf:
            - type: number
            - type: 'null'
          title: Cpc
        brand_position:
          anyOf:
            - type: integer
            - type: 'null'
          title: Brand Position
        competitor_position:
          anyOf:
            - type: integer
            - type: 'null'
          title: Competitor Position
      type: object
      required:
        - keyword
      title: GoogleAdsContestedKeyword
    GoogleAdsCreativeItem:
      properties:
        media_kind:
          type: string
          enum:
            - text
            - image
            - video
          title: Media Kind
        headline:
          anyOf:
            - type: string
            - type: 'null'
          title: Headline
        body:
          anyOf:
            - type: string
            - type: 'null'
          title: Body
        cta:
          anyOf:
            - type: string
            - type: 'null'
          title: Cta
        surface:
          anyOf:
            - type: string
            - type: 'null'
          title: Surface
        snapshot_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Snapshot Url
        image_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Image Url
        started_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Started At
        scraped_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Scraped At
        observed_running_days:
          anyOf:
            - type: integer
            - type: 'null'
          title: Observed Running Days
        video_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Video Url
        landing_page_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Landing Page Url
        verified:
          anyOf:
            - type: boolean
            - type: 'null'
          title: Verified
        position:
          anyOf:
            - type: integer
            - type: 'null'
          title: Position
      type: object
      required:
        - media_kind
      title: GoogleAdsCreativeItem
      description: |-
        One captured creative, normalized server-side (the FE raw dict is
        all-unknown) with the SAME media classification the module's own ad
        gallery uses.
  securitySchemes:
    HTTPBearer:
      type: http
      scheme: bearer

````