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:
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_KEYThe 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
| Scope | Allows |
|---|---|
| account:read | See your plan, limits, usage and credits |
| rules:read | List and view rules, keywords and sources |
| rules:write | Create, edit, pause, resume and delete rules |
| pages:read | List monitored pages (Facebook groups, subreddits, review sites, app stores, web pages) |
| pages:write | Add and remove monitored pages (counts toward your plan) |
| channels:read | List and view alert channels (secrets stay hidden) |
| channels:write | Create, edit, test and delete alert channels |
| feed:read | Search and read matched posts |
| stats:read | Read dashboard and statistics numbers |
| comments:write | Generate 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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Unix 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 }
}
}| Code | Meaning |
|---|---|
invalid_api_key | The key is missing, wrong, expired or revoked. |
insufficient_scope | The key doesn't have the scope this endpoint needs. |
subscription_required | Your plan is inactive; reads may work, writes don't. |
plan_limit_reached | The change would exceed a plan limit or your comment credits. |
rate_limited | Too many requests. Respect Retry-After. |
idempotency_conflict | The Idempotency-Key was reused with a different body. |
api_disabled | The 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:
| Field | Meaning |
|---|---|
limit | keywords, pages or comment_suggestions |
used | What you would have after this change (for comment credits: what you've used today) |
max | What 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
| Task | Endpoint |
|---|---|
| Check plan, usage and credits | GET /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 from | GET /search-countries |
| Pause or resume a rule | POST /rules/{id}/pause, POST /rules/{id}/resume |
| Manage alert channels | /channels, /channels/{id} |
| Send a test alert | POST /channels/{id}/test |
| Search matches | GET /matches |
| Get a match with full text | GET /matches/{id} |
| Draft a reply (1 credit) | POST /matches/{id}/comment-suggestions |
| Dashboard numbers and charts | GET /stats/summary, GET /stats/timeseries |
| Per-source, keyword and rule stats | GET /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 /pageslists the pages in your account, each with itsplatform,source_type,review_statusandurl. Needspages:read.POST /pagesadds pages by URL. Links from supported sites are accepted straight away; other sites getreview_status: "pending"until we've checked them, usually within 1 business day. Links that already exist in your account are returned instead of duplicated. Needspages:write.DELETE /pages/{id}removes a page from your account and from every rule that uses it. Needspages: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-countriesreturns the countries you can use, as{ "data": { "countries": ["VN", "TH"] } }. An empty list means country search isn't available, andsearch_geosmust 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:
| Parameter | Example | Description |
|---|---|---|
rule | r_1,r_2 | Comma-separated rule ids |
source | reddit | Platform, or a source ref like reddit:subreddit:abc |
keyword | crm | Exact matched keyword (case-insensitive) |
sentiment | negative | Comma-separated sentiments |
tags | buy_intent,complaint | Comma-separated tags |
from, to | 2026-10-01 | Day range in UTC, inclusive |
Creating channels
POST /channels supports email, slack, telegram and webhook:
- email: set
destinationto the address. A verification link is sent. - webhook: set
destinationto a public HTTPS URL and, to sign requests,webhook_secret. See Webhooks.PATCHwith a newwebhook_secretrotates it;""removes it. - slack: set
destinationto an incoming-webhook URL (https://hooks.slack.com/...), or leave it empty to getsetup.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
destinationempty. The response includessetup.telegram.deep_link(andgroup_linkfor groups); open it and press Start. PollGET /channels/{id}untiltelegram_pairedistrue.
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
- API reference: every endpoint and schema
- MCP server: the same capabilities for AI agents
- Webhooks: receive matches as they happen