openapi: 3.1.0
info:
title: Scrunch Data Signals API
version: 0.1.0
description: 'Signals surface of the Scrunch Data API: the nightly detection sweep of statistically-tested level changes
and trends in AI visibility metrics, with stable signal fingerprints and team reactions. Split verbatim from the live
OpenAPI served at https://api.scrunchai.com/v1/openapi.json.'
servers:
- url: https://api.scrunchai.com/v1
tags:
- name: Signals
paths:
/{brand_id}/signals:
get:
tags:
- Signals
summary: List Signals
description: 'List detected signals for a brand — statistically-tested movements (level changes and trends) in AI-visibility
metrics, produced by the nightly detection sweep.
## Population
By default only user-facing signals are returned: the confidence tiers `high`, `confident`, `worth_a_look`, and `provisional`.
Pass an explicit `tier` to select a single tier, including the noise-floor tiers. An ongoing signal is re-detected
daily under the same `fingerprint`; a multi-day date range returns only the latest detection per signal identity,
so you never see day-by-day duplicates of the same issue.
## Sorting
`sort=score_desc` (default) orders by engine priority, `delta_desc` by largest absolute change, `detected_desc` by
newest detection date.'
operationId: listSignals
security:
- HTTPBearer:
- query
parameters:
- name: brand_id
in: path
required: true
schema:
type: integer
title: Brand Id
- name: scope
in: query
required: false
schema:
anyOf:
- enum:
- account
- account_platform
- topic
- topic_platform
type: string
- type: 'null'
description: 'Slice granularity the signal was detected on: `account` (whole brand), `account_platform` (brand on
one AI platform), `topic`, or `topic_platform`.'
title: Scope
description: 'Slice granularity the signal was detected on: `account` (whole brand), `account_platform` (brand on
one AI platform), `topic`, or `topic_platform`.'
- name: platform
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: AI platform the signal was detected on (e.g. `OpenAI`). Multi-platform signals carry the `(multi)`
sentinel; the response `slice.platforms` lists the real platforms.
title: Platform
description: AI platform the signal was detected on (e.g. `OpenAI`). Multi-platform signals carry the `(multi)` sentinel;
the response `slice.platforms` lists the real platforms.
- name: metric
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: 'Metric the signal fired on. Known values: `presence_rate`, `position_top_rate`, `cited_domain_rate`
(new metrics may be added).'
title: Metric
description: 'Metric the signal fired on. Known values: `presence_rate`, `position_top_rate`, `cited_domain_rate`
(new metrics may be added).'
- name: alert_type
in: query
required: false
schema:
anyOf:
- enum:
- level_change
- trend
type: string
- type: 'null'
description: 'Detection kind: `level_change` (step shift) or `trend` (sustained drift).'
title: Alert Type
description: 'Detection kind: `level_change` (step shift) or `trend` (sustained drift).'
- name: direction
in: query
required: false
schema:
anyOf:
- enum:
- up
- down
- none
type: string
- type: 'null'
description: 'Direction of the movement: `up`, `down`, or `none`.'
title: Direction
description: 'Direction of the movement: `up`, `down`, or `none`.'
- name: tier
in: query
required: false
schema:
anyOf:
- enum:
- high
- confident
- worth_a_look
- provisional
- low_confidence
- underpowered
- untested
type: string
- type: 'null'
description: Confidence tier. When omitted, only the default user-facing tiers are returned (`high`, `confident`,
`worth_a_look`, `provisional`); pass an explicit tier to see a single tier, including the noise-floor tiers (`low_confidence`,
`underpowered`, `untested`).
title: Tier
description: Confidence tier. When omitted, only the default user-facing tiers are returned (`high`, `confident`,
`worth_a_look`, `provisional`); pass an explicit tier to see a single tier, including the noise-floor tiers (`low_confidence`,
`underpowered`, `untested`).
- name: subject_kind
in: query
required: false
schema:
anyOf:
- enum:
- brand
- competitor
type: string
- type: 'null'
description: 'Whose movement the signal describes: your `brand` or a `competitor`.'
title: Subject Kind
description: 'Whose movement the signal describes: your `brand` or a `competitor`.'
- name: anchor_from
in: query
required: false
schema:
anyOf:
- type: string
format: date
- type: 'null'
description: Earliest `detected_for_date` to include (inclusive, YYYY-MM-DD).
title: Anchor From
description: Earliest `detected_for_date` to include (inclusive, YYYY-MM-DD).
- name: anchor_to
in: query
required: false
schema:
anyOf:
- type: string
format: date
- type: 'null'
description: Latest `detected_for_date` to include (inclusive, YYYY-MM-DD).
title: Anchor To
description: Latest `detected_for_date` to include (inclusive, YYYY-MM-DD).
- name: mover_url
in: query
required: false
schema:
anyOf:
- type: string
minLength: 1
maxLength: 2048
- type: 'null'
description: Case-insensitive substring matched against the URLs in the signal's `url_movers` evidence (per-URL
citation movers, emitted for `cited_domain_rate` signals). Only signals with at least one matching mover URL are
returned; signals without URL movers never match.
title: Mover Url
description: Case-insensitive substring matched against the URLs in the signal's `url_movers` evidence (per-URL citation
movers, emitted for `cited_domain_rate` signals). Only signals with at least one matching mover URL are returned;
signals without URL movers never match.
- name: sort
in: query
required: false
schema:
enum:
- score_desc
- delta_desc
- detected_desc
type: string
description: 'Sort order: `score_desc` (engine priority, default), `delta_desc` (largest absolute change first),
or `detected_desc` (newest first).'
default: score_desc
title: Sort
description: 'Sort order: `score_desc` (engine priority, default), `delta_desc` (largest absolute change first), or
`detected_desc` (newest first).'
- name: limit
in: query
required: false
schema:
type: integer
maximum: 200
minimum: 1
description: Maximum number of signals to return.
default: 50
title: Limit
description: Maximum number of signals to return.
- name: offset
in: query
required: false
schema:
type: integer
minimum: 0
description: Number of signals to skip (pagination).
default: 0
title: Offset
description: Number of signals to skip (pagination).
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/CollectionResponse_APISignal_'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/{brand_id}/signals/anchors:
get:
tags:
- Signals
summary: List Signal Anchor Dates
description: List recent distinct detection dates (`detected_for_date`) that produced user-facing signals, newest first.
Useful for building date pickers or polling for new detection runs — the population matches `listSignals` with default
filters, so every returned day has at least one signal.
operationId: listSignalAnchors
security:
- HTTPBearer:
- query
parameters:
- name: brand_id
in: path
required: true
schema:
type: integer
title: Brand Id
- name: limit
in: query
required: false
schema:
type: integer
maximum: 365
minimum: 1
description: Maximum number of dates to return.
default: 90
title: Limit
description: Maximum number of dates to return.
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/APISignalAnchors'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/{brand_id}/signals/reactions:
get:
tags:
- Signals
summary: List Signal Reactions
description: List reactions users have set on this brand's signals, newest first. This is **brand-wide** — it returns
every user's reactions, not just the caller's — so an integration can export the team's aggregate feedback. Filter
by `fingerprint` to get reactions for one signal identity, or by `reaction` to get e.g. everything marked `useful`.
operationId: listSignalReactions
security:
- HTTPBearer:
- query
parameters:
- name: brand_id
in: path
required: true
schema:
type: integer
title: Brand Id
- name: fingerprint
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Only reactions on this signal fingerprint.
title: Fingerprint
description: Only reactions on this signal fingerprint.
- name: reaction
in: query
required: false
schema:
anyOf:
- enum:
- useful
- not_useful
- dismissed
- actioned
type: string
- type: 'null'
description: Only reactions with this value.
title: Reaction
description: Only reactions with this value.
- name: limit
in: query
required: false
schema:
type: integer
maximum: 200
minimum: 1
description: Maximum number of reactions to return.
default: 50
title: Limit
description: Maximum number of reactions to return.
- name: offset
in: query
required: false
schema:
type: integer
minimum: 0
description: Number of reactions to skip (pagination).
default: 0
title: Offset
description: Number of reactions to skip (pagination).
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/CollectionResponse_APISignalReaction_'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/{brand_id}/signals/{signal_id}:
get:
tags:
- Signals
summary: Get Signal
description: Fetch one signal by ID, including its full narrative (what happened, why it matters, what to do) when available.
Returns 404 for IDs that exist but are not user-facing (non-fired or cluster-child rows) — the same population rule
as `listSignals`.
operationId: getSignal
security:
- HTTPBearer:
- query
parameters:
- name: brand_id
in: path
required: true
schema:
type: integer
title: Brand Id
- name: signal_id
in: path
required: true
schema:
type: integer
title: Signal Id
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/APISignal'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/{brand_id}/signals/{signal_id}/reaction:
put:
tags:
- Signals
summary: Set Signal Reaction
description: 'Set (or replace) the calling user''s reaction on a signal. Reactions are keyed on the signal''s stable
`fingerprint`, so a reaction survives the nightly re-detection of the same underlying issue.
Requires a **user token** (JWT) — API keys have no user identity to attribute the reaction to and receive 403.'
operationId: setSignalReaction
security:
- HTTPBearer:
- query
parameters:
- name: brand_id
in: path
required: true
schema:
type: integer
title: Brand Id
- name: signal_id
in: path
required: true
schema:
type: integer
title: Signal Id
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/APISignalReactionUpsert'
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/APISignalReaction'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
delete:
tags:
- Signals
summary: Clear Signal Reaction
description: Remove the calling user's reaction on a signal's stable identity. Requires a **user token** (JWT); API
keys receive 403.
operationId: clearSignalReaction
security:
- HTTPBearer:
- query
parameters:
- name: brand_id
in: path
required: true
schema:
type: integer
title: Brand Id
- name: signal_id
in: path
required: true
schema:
type: integer
title: Signal Id
responses:
'204':
description: Successful Response
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
components:
schemas:
APISignal:
properties:
id:
type: integer
title: Id
description: Signal ID. Stable within one detection; use for `getSignal`.
detected_for_date:
type: string
format: date
title: Detected For Date
description: Anchor date the nightly detection ran for (YYYY-MM-DD).
fingerprint:
type: string
title: Fingerprint
description: Stable identity of the underlying issue across nightly re-detections (hash of the slice + metric).
An ongoing signal is re-raised daily under the same fingerprint; reactions are keyed on it.
subject_kind:
type: string
enum:
- brand
- competitor
title: Subject Kind
description: 'Whose movement the signal describes: your `brand` or a `competitor`.'
alert_type:
type: string
enum:
- level_change
- trend
title: Alert Type
description: 'Detection kind: `level_change` (step shift) or `trend` (sustained drift).'
scope:
type: string
enum:
- account
- account_platform
- topic
- topic_platform
title: Scope
description: 'Slice granularity: `account`, `account_platform`, `topic`, or `topic_platform`.'
metric:
type: string
title: Metric
description: Metric the signal fired on (e.g. `presence_rate`, `position_top_rate`, `cited_domain_rate`).
platform:
type: string
title: Platform
description: AI platform the signal was detected on. `(multi)` means the slice spans several platforms — see `slice.platforms`
for the real list.
direction:
type: string
enum:
- up
- down
- none
title: Direction
description: 'Direction of the movement: `up`, `down`, `none`.'
tier:
type: string
enum:
- high
- confident
- worth_a_look
- provisional
- low_confidence
- underpowered
- untested
title: Tier
description: 'Confidence tier, best first: `high`, `confident`, `worth_a_look`, `provisional`, then noise-floor
tiers `low_confidence`, `underpowered`, `untested` (hidden by default).'
current_value:
type: number
title: Current Value
description: Metric value over the current window (0-1 rate).
baseline_value:
type: number
title: Baseline Value
description: Metric value over the baseline window (0-1 rate).
delta_absolute:
type: number
title: Delta Absolute
description: '`current_value - baseline_value` (positive = up).'
score:
anyOf:
- type: number
- type: 'null'
title: Score
description: Engine priority score used for the default sort; null for legacy rows.
narrative_what:
type: string
title: Narrative What
description: One-line headline of what changed.
narrative:
anyOf:
- $ref: '#/components/schemas/APISignalNarrative'
- type: 'null'
description: Full narrative (what happened / why it matters / what to do), when available.
slice:
$ref: '#/components/schemas/APISignalSlice'
description: The data slice the signal was detected on.
window_current_start:
anyOf:
- type: string
format: date
- type: 'null'
title: Window Current Start
description: Start of the current comparison window.
window_current_end:
anyOf:
- type: string
format: date
- type: 'null'
title: Window Current End
description: End of the current comparison window.
baseline_definition:
anyOf:
- type: string
- type: 'null'
title: Baseline Definition
description: Human-readable definition of the baseline window.
url_movers:
items:
$ref: '#/components/schemas/APISignalUrlMover'
type: array
title: Url Movers
description: 'Per-URL citation movers behind the signal: the brand/competitor URLs whose distinct-response citation
counts moved most between the baseline and current windows. Emitted for `cited_domain_rate` signals; empty for
other metrics and older signals. Filterable via `mover_url`.'
created_at:
type: string
format: date-time
title: Created At
description: When the signal row was created (UTC).
type: object
required:
- id
- detected_for_date
- fingerprint
- subject_kind
- alert_type
- scope
- metric
- platform
- direction
- tier
- current_value
- baseline_value
- delta_absolute
- narrative_what
- slice
- created_at
title: APISignal
description: A detected, statistically-tested movement in an AI-visibility metric.
APISignalAnchors:
properties:
dates:
items:
type: string
format: date
type: array
title: Dates
description: Distinct `detected_for_date` days with user-facing signals, newest first. Same population as `listSignals`
with default filters.
type: object
required:
- dates
title: APISignalAnchors
description: Recent distinct detection dates that produced signals.
APISignalNarrative:
properties:
what_happened:
anyOf:
- type: string
- type: 'null'
title: What Happened
description: Plain-language description of the detected change.
why_it_matters:
anyOf:
- type: string
- type: 'null'
title: Why It Matters
description: Why the change is significant for the brand.
what_to_do:
anyOf:
- type: string
- type: 'null'
title: What To Do
description: Suggested next step or investigation.
type: object
title: APISignalNarrative
description: Engine-generated narrative for a signal.
APISignalReaction:
properties:
user_id:
type: integer
title: User Id
description: ID of the user the reaction belongs to.
insight_id:
type: integer
title: Insight Id
description: Signal ID the reaction was last set on.
fingerprint:
type: string
title: Fingerprint
description: Stable signal identity the reaction follows across re-detections.
reaction:
type: string
enum:
- useful
- not_useful
- dismissed
- actioned
title: Reaction
description: The reaction value.
reason:
anyOf:
- type: string
- type: 'null'
title: Reason
description: Free-text reason, when provided.
surface:
type: string
enum:
- in_app
- email
- slack
- api
title: Surface
description: 'Where the reaction was set: `in_app`, `email`, `slack`, or `api`.'
created_at:
type: string
format: date-time
title: Created At
description: When the reaction was first set (UTC).
updated_at:
type: string
format: date-time
title: Updated At
description: When the reaction was last changed (UTC).
type: object
required:
- user_id
- insight_id
- fingerprint
- reaction
- surface
- created_at
- updated_at
title: APISignalReaction
description: A user's reaction to a signal's stable identity.
APISignalReactionUpsert:
properties:
reaction:
type: string
enum:
- useful
- not_useful
- dismissed
- actioned
title: Reaction
description: 'Reaction to set: `useful`, `not_useful`, `dismissed`, or `actioned`.'
reason:
anyOf:
- type: string
maxLength: 2000
- type: 'null'
title: Reason
description: Optional free-text reason accompanying the reaction.
type: object
required:
- reaction
title: APISignalReactionUpsert
description: Request body for `setSignalReaction`.
APISignalSlice:
properties:
platforms:
items:
type: string
type: array
title: Platforms
description: AI platforms the slice covers. Empty means all platforms.
topic_labels:
items:
type: string
type: array
title: Topic Labels
description: Topic labels the slice covers. Empty means all topics.
geo_country:
anyOf:
- type: string
- type: 'null'
title: Geo Country
description: Country the slice is restricted to, when geo-scoped.
type: object
title: APISignalSlice
description: The data slice a signal was detected on.
APISignalUrlMover:
properties:
normalized_url:
type: string
title: Normalized Url
description: Normalized cited URL (scheme and tracking params stripped).
owner:
type: string
enum:
- brand
- competitor
title: Owner
description: 'Whose domain the URL belongs to: `brand` or `competitor`.'
competitor_id:
anyOf:
- type: integer
- type: 'null'
title: Competitor Id
description: Competitor ID for `competitor`-owned URLs.
competitor_name:
anyOf:
- type: string
- type: 'null'
title: Competitor Name
description: Competitor name for `competitor`-owned URLs.
current_responses:
type: integer
title: Current Responses
description: Distinct responses citing the URL in the current window.
baseline_responses:
type: integer
title: Baseline Responses
description: Distinct responses citing the URL in the baseline window.
current_prompts:
type: integer
title: Current Prompts
description: Distinct prompts whose responses cite the URL in the current window.
baseline_prompts:
type: integer
title: Baseline Prompts
description: Distinct prompts whose responses cite the URL in the baseline window.
contribution:
anyOf:
- type: number
- type: 'null'
title: Contribution
description: Exact share of the signal's metric delta attributed to this URL. Brand-owned URLs only; null on competitor
entries and older signals.
type: object
required:
- normalized_url
- owner
- current_responses
- baseline_responses
- current_prompts
- baseline_prompts
title: APISignalUrlMover
description: A per-URL citation mover backing a `cited_domain_rate` signal.
AggregationGranularity:
type: string
enum:
- daily
- weekly
- monthly
CollectionResponse_APISignalReaction_:
properties:
total:
type: integer
title: Total
offset:
type: integer
title: Offset
default: 0
limit:
anyOf:
- type: integer
- type: 'null'
title: Limit
items:
items:
$ref: '#/components/schemas/APISignalReaction'
type: array
title: Items
metadata:
anyOf:
- $ref: '#/components/schemas/TimeSeriesMetadata'
- type: 'null'
type: object
required:
- total
- items
title: CollectionResponse[APISignalReaction]
CollectionResponse_APISignal_:
properties:
total:
type: integer
title: Total
offset:
type: integer
title: Offset
default: 0
limit:
anyOf:
- type: integer
- type: 'null'
title: Limit
items:
items:
$ref: '#/components/schemas/APISignal'
type: array
title: Items
metadata:
anyOf:
- $ref: '#/components/schemas/TimeSeriesMetadata'
- type: 'null'
type: object
required:
- total
- items
title: CollectionResponse[APISignal]
HTTPValidationError:
properties:
detail:
items:
$ref: '#/components/schemas/ValidationError'
type: array
title: Detail
type: object
title: HTTPValidationError
OwnerTimeSeriesPoint:
properties:
time_bucket:
type: string
title: Time Bucket
description: Date string for the time bucket (YYYY-MM-DD)
brand:
type: integer
title: Brand
description: Observation count for brand-owned domains
competitor:
type: integer
title: Competitor
description: Observation count for competitor-owned domains
other:
type: integer
title: Other
description: Observation count for third-party domains
type: object
required:
- time_bucket
- brand
- competitor
- other
title: OwnerTimeSeriesPoint
description: A single time bucket with observation counts by owner type.
SegmentTotal:
properties:
name:
type: string
title: Name
observation_count:
type: integer
title: Observation Count
type: object
required:
- name
- observation_count
title: SegmentTotal
description: Observation count for a citation segment, computed server-side.
TimeSeriesMetadata:
properties:
aggregation_granularity:
$ref: '#/components/schemas/AggregationGranularity'
description: 'The aggregation granularity: daily, weekly, or monthly'
period_count:
type: integer
title: Period Count
description: The number of periods in the time series
start_date:
type: string
format: date-time
title: Start Date
description: The start date of the time series
end_date:
type: string
format: date-time
title: End Date
description: The end date of the time series
top_domains:
anyOf:
- $ref: '#/components/schemas/TopDomainsMetadata'
- type: 'null'
description: Top domains metadata (only populated by sources/domains endpoints)
type: object
required:
- aggregation_granularity
- period_count
- start_date
- end_date
title: TimeSeriesMetadata
TopDomainSummary:
properties:
domain:
type: string
title: Domain
domain_owner:
type: string
title: Domain Owner
observation_count:
type: integer
title: Observation Count
type: object
required:
- domain
- domain_owner
- observation_count
title: TopDomainSummary
TopDomainsMetadata:
properties:
domains:
items:
$ref: '#/components/schemas/TopDomainSummary'
type: array
title: Domains
description: Top domains by observation count (always domain-level aggregated)
grand_total:
type: integer
title: Grand Total
description: Total observation count across all domains (for calculating 'Other')
owner_totals:
additionalProperties:
type: integer
type: object
title: Owner Totals
description: Observation counts by owner type (brand, competitor, other)
owner_time_series:
anyOf:
- items:
$ref: '#/components/schemas/OwnerTimeSeriesPoint'
type: array
- type: 'null'
title: Owner Time Series
descripti
# --- truncated at 32 KB (33 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/scrunch-ai/refs/heads/main/openapi/scrunch-ai-signals-api-openapi.yml