BuzzGleanBuzzGlean
REST API

REST API

Manage rules, pages and alert channels, search matches and read statistics from your own scripts.

The BuzzGlean REST API gives your code the same abilities as the app: create and manage alert rules, pages and channels, search matched posts, generate comment suggestions and read statistics.

  • Base URL: https://signalwatchr-dev.vercel.app/api/public/v1
  • Format: JSON request and response bodies
  • Spec: OpenAPI 3.1 at https://signalwatchr-dev.vercel.app/api/public/v1/openapi.json. Import it into Postman, Insomnia or a client generator.
  • Reference: every endpoint, parameter and schema is listed in the API reference.

Quickstart

Create an API key

In the app, open API & MCP in the sidebar and click Create API key. Choose what the key can do (see scopes) and an optional expiry. Copy the key: it's shown only once.

Make your first request

Send the key as a bearer token. GET /me returns your plan, limits, usage and credits:

Terminal
curl https://signalwatchr-dev.vercel.app/api/public/v1/me \
  -H "Authorization: Bearer YOUR_API_KEY"

Read your matches

The examples below use $BASE_URL for the base URL above.

curl "$BASE_URL/matches?tags=buy_intent&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

Authentication

Every request needs an API key in the Authorization header:

Authorization: Bearer YOUR_API_KEY

The API is server-to-server only: responses have no CORS headers, so don't call it from a browser, where your key would be exposed.

API keys

Manage keys under API & MCP in the app.

  • Access: Read only (view account, rules, pages, channels, matches and stats), Full access (everything, including creating and deleting), or Custom (pick exact scopes).
  • Expiry: never, or after a set number of days. Expiry can't be changed later; rotate the key to reset it. We email you before a key expires.
  • Rotate: creates a new key with the same name and access. The old key keeps working for a grace period you choose (immediately up to several days), so you can deploy the new one without downtime.
  • Revoke: the key stops working immediately.
  • Activity: open a key to see requests per day (REST and MCP) over the last 30 days and its recent write actions.

You can have up to 10 active keys. Treat keys like passwords: they act on your whole account.

Scopes

ScopeAllows
account:readSee your plan, limits, usage and credits
rules:readList and view rules, keywords and sources
rules:writeCreate, edit, pause, resume and delete rules
pages:readList monitored pages (Facebook groups, subreddits, review sites, app stores, web pages)
pages:writeAdd and remove monitored pages (counts toward your plan)
channels:readList and view alert channels (secrets stay hidden)
channels:writeCreate, edit, test and delete alert channels
feed:readSearch and read matched posts
stats:readRead dashboard and statistics numbers
comments:writeGenerate comment suggestions (uses credits)

A request without the needed scope fails with 403 insufficient_scope.

Conventions

Pagination

List endpoints take limit (1-100, default 25) and cursor. Responses look like:

{ "data": [/* ... */], "nextCursor": "eyJpZCI6..." }

Pass nextCursor as cursor to get the next page. When nextCursor is null, you've reached the end.

Partial updates

PATCH endpoints accept only the fields you want to change. Field names match what GET returns, so you can fetch a resource, modify it and send it back.

Idempotency

POST endpoints accept an Idempotency-Key header. Retrying with the same key and body within 24 hours returns the original response instead of creating a duplicate. Reusing a key with a different body fails with idempotency_conflict.

curl -X POST "$BASE_URL/rules" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 7f6c2b1e-rule-crm" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CRM recommendations",
    "prompt_text": "People asking for a simple CRM for a small sales team",
    "keywords": ["CRM", "recommend"],
    "search_keywords_sources": ["facebook", "reddit"],
    "search_geos": ["VN", "TH"],
    "page_ids": ["<page id>"],
    "alert_channel_ids": ["<channel id>"]
  }'

Rate limits

Limits are per account and shared by all your keys and MCP sessions. Every response includes:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetUnix time (seconds) when a request slot frees up

When you exceed the limit you get 429 rate_limited with a Retry-After header. Wait that long before retrying. See Plans and billing for each plan's limit.

Endpoints that run an AI model, such as comment suggestions, have their own limit on top of the plan limit: 10 requests per minute and 200 per day per account. Going over it also returns 429 rate_limited with a Retry-After header, and error.details says which window (minute or day) was hit.

Errors

Errors use one shape:

{
  "error": {
    "code": "plan_limit_reached",
    "message": "Keyword limit reached: this change needs 12 keywords, your plan allows 10. Remove keywords from a rule or upgrade in Billing: https://…/app/billing",
    "details": { "limit": "keywords", "used": 12, "max": 10 }
  }
}
CodeMeaning
invalid_api_keyThe key is missing, wrong, expired or revoked.
insufficient_scopeThe key doesn't have the scope this endpoint needs.
subscription_requiredYour plan is inactive; reads may work, writes don't.
plan_limit_reachedThe change would exceed a plan limit or your comment credits.
rate_limitedToo many requests. Respect Retry-After.
idempotency_conflictThe Idempotency-Key was reused with a different body.
api_disabledThe API is temporarily disabled.

Plan limits

Plan limits and comment credits work exactly as in the app. Accounts whose plan is locked can read but not write.

When a create or update would go over a limit, the request fails with 403 plan_limit_reached and nothing is saved. error.details has:

FieldMeaning
limitkeywords, pages or comment_suggestions
usedWhat you would have after this change (for comment credits: what you've used today)
maxWhat your plan allows

How usage is counted:

  • Keywords are counted once across all your rules, ignoring case and extra spaces. A keyword used by three rules counts as 1. Keywords count whether or not they are searched.
  • Pages are the pages in your account, plus 1 for each keyword searched on each platform (search_keywords_sources) and each country it's searched from (search_geos). Searching 3 keywords on Reddit and X counts as 6 pages; searching them from 2 countries counts as 12. With no countries (anywhere) each keyword and platform counts once.
  • A change that doesn't increase your usage is always allowed, so you can still edit rules after a downgrade leaves you over a limit.

Check your usage first with GET /me.

Common tasks

TaskEndpoint
Check plan, usage and creditsGET /me
List, add and remove pages (sources)GET /pages, POST /pages, DELETE /pages/{id}
List, create, update, delete rules/rules, /rules/{id}
List countries keyword searches can run fromGET /search-countries
Pause or resume a rulePOST /rules/{id}/pause, POST /rules/{id}/resume
Manage alert channels/channels, /channels/{id}
Send a test alertPOST /channels/{id}/test
Search matchesGET /matches
Get a match with full textGET /matches/{id}
Draft a reply (1 credit)POST /matches/{id}/comment-suggestions
Dashboard numbers and chartsGET /stats/summary, GET /stats/timeseries
Per-source, keyword and rule statsGET /stats/sources, /stats/keywords, /stats/rules

Pages and rules

A page is a source: a Facebook group, subreddit, review site page, app store listing, or blog or forum page. See Sources.

  • GET /pages lists the pages in your account, each with its platform, source_type, review_status and url. Needs pages:read.
  • POST /pages adds pages by URL. Links from supported sites are accepted straight away; other sites get review_status: "pending" until we've checked them, usually within 1 business day. Links that already exist in your account are returned instead of duplicated. Needs pages:write.
  • DELETE /pages/{id} removes a page from your account and from every rule that uses it. Needs pages:write.

Each new page counts as 1 toward your plan's pages limit; adding more than your plan allows fails with plan_limit_reached.

Rules reference their sources with page_ids, the ids returned by /pages. A rule can include pages that are still in review; they're monitored once approved.

Rules can also search whole platforms for their keywords: list the platforms in search_keywords_sources (for example ["facebook", "reddit"]). To run those searches from specific countries, set search_geos to ISO country codes (for example ["VN", "TH"]). Leave it empty to search from anywhere.

  • GET /search-countries returns the countries you can use, as { "data": { "countries": ["VN", "TH"] } }. An empty list means country search isn't available, and search_geos must stay empty.
  • Codes not in that list are rejected with 400. A country already saved on a rule stays valid even if it's later removed from the list, but it isn't searched anymore.
  • Each country is a separate search: 1 page per keyword, platform and country.

fb_group_ids and subreddit_ids on rules are deprecated aliases of page_ids. They still work: the Facebook group ids or subreddit names you send are matched to your pages, or added as new pages (which count toward your plan). In responses they're derived from page_ids. Use page_ids in new code.

Filtering matches

GET /matches accepts:

ParameterExampleDescription
ruler_1,r_2Comma-separated rule ids
sourceredditPlatform, or a source ref like reddit:subreddit:abc
keywordcrmExact matched keyword (case-insensitive)
sentimentnegativeComma-separated sentiments
tagsbuy_intent,complaintComma-separated tags
from, to2026-10-01Day range in UTC, inclusive

Creating channels

POST /channels supports email, slack, telegram and webhook:

  • email: set destination to the address. A verification link is sent.
  • webhook: set destination to a public HTTPS URL and, to sign requests, webhook_secret. See Webhooks. PATCH with a new webhook_secret rotates it; "" removes it.
  • slack: set destination to an incoming-webhook URL (https://hooks.slack.com/...), or leave it empty to get setup.oauth_url. Open that URL in a browser; Slack asks which channel to post to, and the channel is verified when you return.
  • telegram: leave destination empty. The response includes setup.telegram.deep_link (and group_link for groups); open it and press Start. Poll GET /channels/{id} until telegram_paired is true.

POST /channels/{id}/test sends a test alert, or returns the next setup step (oauth_url, telegram) if the channel isn't connected yet. Channel secrets are write-only: responses mask URLs and report has_webhook_secret instead.

Next

On this page