BuzzGleanBuzzGlean
REST API

API reference

Every REST endpoint, parameter and schema, generated from the OpenAPI document.

This reference is generated from the same OpenAPI document served at https://signalwatchr-dev.vercel.app/api/public/v1/openapi.json, so it always matches the live API. For authentication, pagination, errors and rate limits, see the REST API overview.

Base URL: https://signalwatchr-dev.vercel.app/api/public/v1

Account

get/me

Plan, billing state, limits, usage and credits. Required scope: account:read.

Response 200

NameTypeDescription
data*Account

Rules

get/rules

List rules. Required scope: rules:read.

Parameters

NameTypeDescription
status (query)"active" | "paused" | "disabled"
q (query)string
limit (query)integerPage size (1-100, default 25) Default: 25
cursor (query)stringCursor from the previous page's nextCursor

Response 200

NameTypeDescription
data*Rule[]
nextCursor*string | nullPass as `cursor` to get the next page; null = last page

post/rules

Create a rule. Keyword and page limits of your plan apply (same rules as the app): only active pages and keywords of active rules count. Keywords count once across all rules, each page once, and each keyword searched on a platform counts as a page — once per country in search_geos (empty = anywhere = once). search_geos must be codes from GET /search-countries (400 otherwise). Facebook is a keyword-search platform too. inactive_page_ids / inactive_keywords stay on the rule without counting. Over the limit: 403 plan_limit_reached. Required scope: rules:write.

Parameters

NameTypeDescription
Idempotency-Key (header)stringRetry-safe key (≤255 chars). Same key + same body replays the stored response for 24h; a different body returns 409 idempotency_conflict.

Request body

NameTypeDescription
namestring | nullDisplay name
prompt_text*stringWhat a matching post looks like, in plain language. The AI scores posts against it.
thresholdnumberMinimum AI relevance score (0-1) for a match. Default 0.7. Default: 0.7
page_idsstring[]Ids of your pages (see list_pages / GET /pages) to monitor Default: []
inactive_page_idsstring[]Pages kept on the rule but turned off: not monitored and not counted toward plan usage. Omit to keep the current ones.
alert_channel_idsstring[]Alert channel ids that receive matches Default: []
keywordsstring[]Keywords to search for. Monitored on the platforms in search_keywords_sources. Default: []
inactive_keywordsstring[]Keywords kept on the rule but turned off: not searched or matched and not counted toward plan usage. Omit to keep the current ones.
custom_tagsstring[]Default: []
sentiment_analysisobjectDefault: {"enable":false}
search_keywords_sourcesstring[]Platforms searched for the keywords: facebook, reddit, x, linkedin, tiktok, youtube.
search_geosstring[]ISO-2 countries the keyword searches run from. Empty = anywhere. Must be one of GET /api/public/v1/search-countries. Each country is a separate search and counts as one page unit per keyword and platform.
status"active" | "paused"Default: "active"
fb_group_idsstring[]Deprecated: use page_ids. Facebook group ids; missing ones are added as pages.
subreddit_idsstring[]Deprecated: use page_ids. Subreddit names; missing ones are added as pages.

Response 201

NameTypeDescription
data*Rule

get/rules/{id}

Get a rule. Required scope: rules:read.

Parameters

NameTypeDescription
id (path)*string

Response 200

NameTypeDescription
data*Rule

patch/rules/{id}

Update a rule (partial). Omitted fields keep their current values. Turn pages or keywords off/on with inactive_page_ids / inactive_keywords (alone, they keep the rule's sources and only change which are off). Keyword and page limits are checked on every update; a change that doesn't raise usage is always allowed. Over the limit: 403 plan_limit_reached. Required scope: rules:write.

Parameters

NameTypeDescription
id (path)*string

Request body

NameTypeDescription
namestring | null
prompt_textstring
thresholdnumber
page_idsstring[]Active pages. Pages turned off stay in inactive_page_ids unless listed here.
inactive_page_idsstring[]Pages kept on the rule but turned off: not monitored and not counted toward plan usage. Omit to keep the current ones.
fb_group_idsstring[]Deprecated: use page_ids. Replaces the rule's Facebook groups.
subreddit_idsstring[]Deprecated: use page_ids. Replaces the rule's subreddits.
alert_channel_idsstring[]
keywordsstring[]Keywords to search for. Monitored on the platforms in search_keywords_sources.
inactive_keywordsstring[]Keywords kept on the rule but turned off: not searched or matched and not counted toward plan usage. Omit to keep the current ones.
custom_tagsstring[]
sentiment_analysisobject
search_keywords_sourcesstring[]Platforms searched for the keywords: facebook, reddit, x, linkedin, tiktok, youtube.
search_geosstring[]ISO-2 countries the keyword searches run from. Empty = anywhere. Must be one of GET /api/public/v1/search-countries. Each country is a separate search and counts as one page unit per keyword and platform.
status"active" | "paused"

Response 200

NameTypeDescription
data*Rule

delete/rules/{id}

Delete a rule. Required scope: rules:write.

Parameters

NameTypeDescription
id (path)*string

Response 204

No content.

post/rules/{id}/pause

Pause a rule. A paused rule's pages and keywords stop counting toward plan usage. Required scope: rules:write.

Parameters

NameTypeDescription
id (path)*string
Idempotency-Key (header)stringRetry-safe key (≤255 chars). Same key + same body replays the stored response for 24h; a different body returns 409 idempotency_conflict.

Response 200

NameTypeDescription
data*Rule

post/rules/{id}/resume

Resume a paused rule. The rule's active pages and keywords count toward plan usage again. Over the limit: 403 plan_limit_reached. Required scope: rules:write.

Parameters

NameTypeDescription
id (path)*string
Idempotency-Key (header)stringRetry-safe key (≤255 chars). Same key + same body replays the stored response for 24h; a different body returns 409 idempotency_conflict.

Response 200

NameTypeDescription
data*Rule

get/search-countries

Countries keyword searches can run from. ISO-2 codes allowed in a rule's search_geos. Each country is a separate search and counts as one page per keyword and platform. An empty list means only "anywhere" searches are available (leave search_geos empty). Required scope: rules:read.

Response 200

NameTypeDescription
data*SearchCountries

Pages

get/pages

List monitored pages. Facebook groups, subreddits, review sites, app stores and web pages you monitor. Use their ids in a rule's page_ids. Required scope: pages:read.

Parameters

NameTypeDescription
q (query)stringSearch name, URL or id
kind (query)"fb_group" | "subreddit" | "web"
review_status (query)"accepted" | "pending" | "rejected"
limit (query)integerPage size (1-100, default 25) Default: 25
cursor (query)stringCursor from the previous page's nextCursor

Response 200

NameTypeDescription
data*Page[]
nextCursor*string | nullPass as `cursor` to get the next page; null = last page

post/pages

Add a page by URL. Facebook group, subreddit, or any other page. Sites not in the accepted list are reviewed first (review_status pending). Returns the existing page if it's already listed. Invalid links return 422 invalid_url with details.errors[{url, code, message}]. Each page counts toward your plan. Required scope: pages:write.

Parameters

NameTypeDescription
Idempotency-Key (header)stringRetry-safe key (≤255 chars). Same key + same body replays the stored response for 24h; a different body returns 409 idempotency_conflict.

Request body

NameTypeDescription
url*stringLink to a Facebook group, subreddit, review/app store page, blog or forum

Response 201

NameTypeDescription
data*Page

delete/pages/{id}

Remove a page. Rules stop monitoring it. Required scope: pages:write.

Parameters

NameTypeDescription
id (path)*string

Response 204

No content.

Channels

get/channels

List alert channels. Required scope: channels:read.

Parameters

NameTypeDescription
type (query)"email" | "slack" | "telegram" | "webhook"
status (query)"active" | "disabled"
limit (query)integerPage size (1-100, default 25) Default: 25
cursor (query)stringCursor from the previous page's nextCursor

Response 200

NameTypeDescription
data*Channel[]
nextCursor*string | nullPass as `cursor` to get the next page; null = last page

post/channels

Create an alert channel. Types: email, slack, telegram, webhook. email sends a verification link. slack: pass an incoming-webhook URL, or omit destination to get setup.oauth_url (open it in a browser to Add to Slack). telegram returns setup.telegram.deep_link; open it and press Start to pair a chat. webhook: optional webhook_secret signs each request. Required scope: channels:write.

Parameters

NameTypeDescription
Idempotency-Key (header)stringRetry-safe key (≤255 chars). Same key + same body replays the stored response for 24h; a different body returns 409 idempotency_conflict.

Request body

NameTypeDescription
type*"email" | "slack" | "telegram" | "webhook"Channel type. email sends a verification link. slack: pass an incoming-webhook URL, or leave destination empty to get setup.oauth_url (Add to Slack). telegram returns setup.telegram.deep_link to pair a chat.
destinationstringemail: the address. webhook: a public HTTPS URL. slack: an incoming-webhook URL (https://hooks.slack.com/...) or empty for Add to Slack. telegram: leave empty. Default: ""
settingsobjectemail: { send_interval_minutes } (minimum minutes between emails, default 60). slack: { channel_name } (label). Other keys are ignored. Default: {}
status"active" | "disabled"Default: "active"
webhook_secretstringWebhook only: signing secret (write-only). Requests then carry X-App-Signature: sha256=<HMAC of the body>.

Response 201

NameTypeDescription
data*Channel
setup*object

get/channels/{id}

Get an alert channel. Required scope: channels:read.

Parameters

NameTypeDescription
id (path)*string

Response 200

NameTypeDescription
id*string
type*string
destination*stringEmail address, or a masked URL/identifier for other types
status*"active" | "disabled"
verification_status*"pending" | "verified" | "failed" | null
is_verified*boolean
has_webhook_secret*boolean
slack_channel_id*string | null
slack_connected*booleanSlack: connected with Add to Slack
telegram_paired*boolean
last_verified_at*string | null
last_test_sent_at*string | null
created_at*string | null
updated_at*string | null

patch/channels/{id}

Update an alert channel. Changing the destination restarts verification. webhook_secret sets a new signing secret ("" removes it). Required scope: channels:write.

Parameters

NameTypeDescription
id (path)*string

Request body

NameTypeDescription
destinationstring
settingsobject
status"active" | "disabled"
webhook_secretstringWebhook only: new signing secret; "" removes it.

Response 200

NameTypeDescription
data*Channel
setup*object

delete/channels/{id}

Delete an alert channel. Fails while the channel is linked to an active rule. Required scope: channels:write.

Parameters

NameTypeDescription
id (path)*string

Response 204

No content.

post/channels/{id}/test

Send a test alert (or finish setup). Sends a sample alert. If the channel can't receive one yet, result has the next setup step instead: a resent email link, oauth_url for Slack, or telegram.deep_link. Required scope: channels:write.

Parameters

NameTypeDescription
id (path)*string
Idempotency-Key (header)stringRetry-safe key (≤255 chars). Same key + same body replays the stored response for 24h; a different body returns 409 idempotency_conflict.

Response 200

NameTypeDescription
data*Channel | null
result*object

Matches

get/matches

Search matched posts (newest first). Required scope: feed:read.

Parameters

NameTypeDescription
rule (query)stringComma-separated rule ids
source (query)stringPlatform ("facebook", "reddit", "x", a site id like "trustpilot"), a category ("review_site", "app_store"), "page" for every page source, or a source ref like "reddit:subreddit:abc"
keyword (query)stringExact matched keyword (case-insensitive)
sentiment (query)stringComma-separated sentiments: positive, neutral, negative
tags (query)stringComma-separated tags: buy_intent, recommendation_request, alternative_seeking, complaint, praise, question, feedback, other
from (query)stringStart day (UTC), YYYY-MM-DD, inclusive
to (query)stringEnd day (UTC), YYYY-MM-DD, inclusive
limit (query)integerPage size (1-100, default 25) Default: 25
cursor (query)stringCursor from the previous page's nextCursor

Response 200

NameTypeDescription
data*Match[]
nextCursor*string | nullPass as `cursor` to get the next page; null = last page

get/matches/{id}

Get a match with the full post text. Required scope: feed:read.

Parameters

NameTypeDescription
id (path)*string

Response 200

NameTypeDescription
data*Match

post/matches/{id}/comment-suggestions

Generate a comment suggestion (uses 1 credit). Required scope: comments:write.

Parameters

NameTypeDescription
id (path)*string
Idempotency-Key (header)stringRetry-safe key (≤255 chars). Same key + same body replays the stored response for 24h; a different body returns 409 idempotency_conflict.

Request body

NameTypeDescription
contextstringWhat the comment should achieve, e.g. "Offer help and mention our free plan". Defaults to your default purpose in the app.

Response 201

NameTypeDescription
data*CommentSuggestion

Stats

get/stats/summary

Dashboard summary: matches, filtering, usage, credits. Required scope: stats:read.

Parameters

NameTypeDescription
range (query)"7d" | "30d" | "90d" | "custom"Default: "7d"
from (query)string
to (query)string

Response 200

NameTypeDescription
data*objectSame numbers as the app's Dashboard / Statistics

get/stats/timeseries

Matches per day with an optional breakdown. Required scope: stats:read.

Parameters

NameTypeDescription
range (query)"7d" | "30d" | "90d" | "custom"Default: "7d"
from (query)string
to (query)string
rules (query)string
breakdown (query)"none" | "platform" | "rule" | "result" | "sentiment" | "tag"Default: "platform"

Response 200

NameTypeDescription
data*objectSame numbers as the app's Dashboard / Statistics

get/stats/sources

Per-source statistics. Required scope: stats:read.

Parameters

NameTypeDescription
range (query)"7d" | "30d" | "90d" | "custom"Default: "7d"
from (query)string
to (query)string
rules (query)string
search (query)stringDefault: ""
sort (query)"name" | "postsCollected" | "keywordHits" | "matched" | "matchRate" | "lastMatchAt" | "rulesCount"Default: "matched"
order (query)"asc" | "desc"Default: "desc"
page (query)integerDefault: 1

Response 200

NameTypeDescription
data*objectSame numbers as the app's Dashboard / Statistics

get/stats/keywords

Per-keyword statistics. Required scope: stats:read.

Parameters

NameTypeDescription
range (query)"7d" | "30d" | "90d" | "custom"Default: "7d"
from (query)string
to (query)string
rules (query)string
search (query)stringDefault: ""
sort (query)"keyword" | "keywordHits" | "matched" | "matchRate" | "lastMatchAt"Default: "matched"
order (query)"asc" | "desc"Default: "desc"
page (query)integerDefault: 1

Response 200

NameTypeDescription
data*objectSame numbers as the app's Dashboard / Statistics

get/stats/rules

Per-rule statistics. Required scope: stats:read.

Parameters

NameTypeDescription
range (query)"7d" | "30d" | "90d" | "custom"Default: "7d"
from (query)string
to (query)string
rules (query)string
search (query)stringDefault: ""
sort (query)"name" | "matched" | "matchRate" | "alertsSent" | "commentSuggestions" | "lastMatchAt"Default: "matched"
order (query)"asc" | "desc"Default: "desc"
page (query)integerDefault: 1

Response 200

NameTypeDescription
data*objectSame numbers as the app's Dashboard / Statistics

Schemas

Error

NameTypeDescription
error*object

Account

NameTypeDescription
plan*"pro" | "max" | "enterprise" | null
interval*"monthly" | "yearly" | null
billing_state*string
trial_ends_at*string | null
current_period_end*string | null
cancel_at_period_end*boolean
limits*objectPlan limits (Enterprise: custom limits)
usage*object
over_limit*boolean
credits*object
api*object

RuleList

NameTypeDescription
data*Rule[]
nextCursor*string | nullPass as `cursor` to get the next page; null = last page

Rule

NameTypeDescription
id*string
name*string | null
prompt_text*string
threshold*number
status*"active" | "paused" | "disabled"
keywords*string[]Active keywords
inactive_keywords*string[]Keywords kept on the rule but turned off: not searched or matched, and not counted toward plan usage
search_keywords_sources*string[]
search_geos*string[]ISO-2 countries the keyword searches run from (empty = anywhere). Countries no longer offered stay listed but aren't searched.
page_ids*string[]Ids of the active pages this rule monitors (see /pages)
inactive_page_ids*string[]Pages kept on the rule but turned off: not monitored, and not counted toward plan usage
fb_group_ids*string[]Deprecated: derived from page_ids (Facebook group ids)
subreddit_ids*string[]Deprecated: derived from page_ids (subreddit names)
alert_channel_ids*string[]
custom_tags*string[]
sentiment_analysis*object
created_at*string | null
updated_at*string | null

SearchCountries

NameTypeDescription
countries*string[]ISO-2 codes usable in a rule's search_geos. Empty = searching from specific countries isn't available.

PageList

NameTypeDescription
data*Page[]
nextCursor*string | nullPass as `cursor` to get the next page; null = last page

Page

NameTypeDescription
id*string
kind*"fb_group" | "subreddit" | "web"
platform*stringfacebook, reddit, a site id (trustpilot, g2, capterra, apple_app_store, google_play) or web
source_type*"group" | "subreddit" | "page"
source_id*stringShared crawl key: FB group id, subreddit name, or w_<hash>
url*string
domain*string
name*string | null
category*"community" | "review_site" | "app_store" | "blog" | "forum" | "web"
review_status*"accepted" | "pending" | "rejected"pending = new site waiting for review; starts once accepted
review_note*string | null
status*"active" | "starting" | "awaiting_access" | "in_review" | "rejected" | "no_access"Monitoring status
last_scanned_at*string | null
created_at*string | null
updated_at*string | null

ChannelList

NameTypeDescription
data*Channel[]
nextCursor*string | nullPass as `cursor` to get the next page; null = last page

Channel

NameTypeDescription
id*string
type*string
destination*stringEmail address, or a masked URL/identifier for other types
status*"active" | "disabled"
verification_status*"pending" | "verified" | "failed" | null
is_verified*boolean
has_webhook_secret*boolean
slack_channel_id*string | null
slack_connected*booleanSlack: connected with Add to Slack
telegram_paired*boolean
last_verified_at*string | null
last_test_sent_at*string | null
created_at*string | null
updated_at*string | null

MatchList

NameTypeDescription
data*Match[]
nextCursor*string | nullPass as `cursor` to get the next page; null = last page

Match

NameTypeDescription
id*stringMatch id (use for get_match / comment suggestions)
post_id*string
rule_id*string
rule_prompt*string | null
platform*string
url*string | nullLink to the full post
author*string | null
text*string
text_truncated*boolean
score*number | nullAI relevance score 0-1
reason*string | nullWhy the AI matched it
sentiment*string | null
tags*string[]
matched_keywords*string[]
status*string | null
matched_at*string | null
posted_at*string | null
feedback*"helpful" | "not_helpful" | null

CommentSuggestion

NameTypeDescription
match_id*string
post_id*string
text*string
language*string | null
purpose*string | null
generation_count*number
credits*object