Endpoints
All ten monitoring endpoints, with request shapes and what each one returns, from creating a monitor through reading its deliveries.
Ten endpoints, all under /reddit/monitor. Every one is authenticated with
your API key and none of them consume API credits. Monitoring is billed as a
plan, and management calls are free.
Monitors
POST /reddit/monitor/add
Create a monitor. Body is { "filter_spec": { ... } }, described in
Filters.
By default every match is delivered to every active webhook on your
account. Pass webhook_ids: ["wh_..."] to route this monitor's matches to
only specific webhook(s) instead -- omit it, or pass an empty array, to keep
the default account-wide behavior. Every id must be a webhook you own, or the
request is rejected with webhook_not_found (400).
Returns the created monitor including its id and effective cadence_s.
Returns 402 when you are at your plan's slot ceiling. The response names the limit and your current tier rather than failing generically.
GET /reddit/monitor/list
All your monitors, plus a slots object with used, total and tier.
A webhook_ids of null here does not mean the monitor has nowhere to
deliver. Null is the default and means every active webhook on the account,
which is the normal healthy state; only a non-empty array narrows delivery. So
do not read a null as a missing destination when a monitor looks quiet. To see
where a monitor's matches actually went, use GET /reddit/monitor/deliveries
below, whose rows carry the resolved webhook_id.
POST /reddit/monitor/update
Change a monitor's filter_spec, pause and resume it with active, or
re-target which webhook(s) it delivers to with webhook_ids.
Editing a filter does not replay history. The monitor stays forward-only from now, not from when it was first created.
webhook_ids replaces the monitor's targeting outright when present --
it does not merge with whatever was set before. Pass an empty array to clear
targeting back to "every active webhook on the account"; omit the field
entirely to leave the monitor's current targeting untouched.
POST /reddit/monitor/remove
Delete a monitor. It stops matching immediately, disappears from
/reddit/monitor/list, and its slot frees up for a new one. There is no undo
endpoint, but its past deliveries are not erased -- /reddit/monitor/deliveries
keeps returning them, both scoped to that id and in the aggregate no-id view,
indefinitely.
GET /reddit/monitor/health?id=<monitor_id>
Per-monitor health for the last 24 hours:
| Field | Meaning |
|---|---|
delivered_24h | Successful deliveries |
failed_24h | Deliveries that exhausted the retry ladder |
suppressed_24h | Alerts deliberately not sent. Two things can do that and suppressed_breakdown says which: your daily delivery ceiling, and the freshness gate that withholds items that were already old when we first saw them. A floor rather than a total: we store at most 500 ceiling-suppression records per monitor per UTC day, so a number at or near 500 means "at least this many". See Limits. |
suppressed_breakdown | Why those alerts were withheld: ceiling, stale, and unknown counts over the same window, plus sampled and sample_truncated. resolved is false when we could not attribute every one of them (unresolved_reason says why), and on false you should not read the counts as a split of the total. |
ceiling_reached | Whether the ceiling specifically suppressed anything for this monitor in the last 24 hours. Stale withholds do not set it, because no limit rejected them and raising your ceiling would not change them. Read as "this monitor has been over its ceiling recently", not as "it is over it right now": the ceiling itself resets at midnight UTC while this window does not, so it can still read true for up to a day after the monitor started delivering again. It also stays true while suppressed_breakdown.resolved is false, because an unattributed suppression is not evidence that the ceiling was not involved. |
last_match_at | Timestamp of the most recent delivery row of ANY status (delivered, failed, dead, or suppressed) -- not filtered to successes. A monitor whose recent matches were all suppressed still shows a fresh timestamp here, whether the ceiling or the freshness gate withheld them; check delivered_24h, suppressed_24h and suppressed_breakdown to tell them apart. |
coverage_24h | Whether every check on this monitor's feeds got all the way back to where the previous check finished. See below -- this is a different question from the counters above, and they cannot answer it. |
stream_liveness | Whether this monitor's feeds are being checked at all, feed by feed. Read this one first: it is the only field that can tell a quiet subreddit apart from a feed nothing is looking at. |
suppressed_24h exists so a match we chose not to send is visible rather than
looking like a quiet day. A match we chose not to send is not the same as no
match, and you should be able to tell the difference without asking us.
When delivered_24h sits far below the number of matches
The usual cause is the freshness gate, and it is the one case where a monitor is working correctly and still looks broken. We only deliver an item that was still fresh when we first saw it. Reddit sometimes surfaces old content into the listing we poll -- a page walk reaching back past where the previous one finished, or a post resurfacing long after it was created -- and delivering a two-week-old post as if it had just appeared is worse than not delivering it.
So those matches are withheld, and every one of them is recorded:
- counted in
suppressed_24hand broken out assuppressed_breakdown.stale, - readable individually on
/reddit/monitor/deliveries?status=suppressed, with the posts themselves, underpayload.suppressed.reason=stale_item.
Nothing failed when you see these. Your endpoint is fine, no limit on your
plan rejected them, and retrying cannot recover them: the items were never fresh
at any point we could have sent them. payload.suppressed carries the age of the
oldest one and the threshold it was measured against.
One thing on your side can move the number. A faster cadence shortens the window each check has to cover, which makes a page walk less likely to reach back past where the previous one finished, and that is one of the two ways old content enters the listing we poll. It does nothing about the other one, a post resurfacing long after it was created, and neither is under your control or ours. So a faster cadence may reduce how often this happens; it cannot stop it, and raising your delivery ceiling has no effect on it at all.
coverage_24h, and why the counters above cannot replace it
Every field above counts rows we wrote about posts we fetched. A post we never
fetched was never filtered, so it never matched, so it produced no row of any
status -- not delivered, not failed, and not suppressed (suppressed
means we found a match and chose not to send it, which cannot stand in for a
match we never saw). So on a day when a feed outran us and posts were lost, all
four counters read exactly as they do on a clean day. A zero there means "no
matches recorded", never "nothing was missed".
coverage_24h answers the other question. Read its status first:
status | Meaning |
|---|---|
complete | Every feed was checked recently, and every check reached the point where the previous check finished. Nothing was skipped over. |
degraded | At least one check was cut short and posts were lost. gaps[] lists each occurrence, posts_in_window_at_least is a floor on how many posts existed across them, and posts_missed_estimate is a modelled figure for how many we could not reach. |
partial | Nothing was found, but we did not look everywhere. reason says which look was missing: loss_events_not_observed (a kind of loss went unchecked, see unobserved_events), streams_not_polling (a feed is not being checked at all, so it cannot record a truncated check either), or stream_liveness_unknown (we could not confirm the feeds are being checked). Not a clean bill of health. |
unknown | The check could not be run at all. reason says why. Not a clean bill of health either. |
Two rules for reading it. A null count is not a zero -- it means the figure
is unavailable, and we would rather say so than print a reassuring number we
cannot support. And posts_in_window_at_least and posts_missed_estimate are
different kinds of number: the first follows from what we counted, the second
is a model of a feed's rate multiplied by a duration, and it leans high on
purpose. They are never merged into one "posts lost" figure.
stream_liveness, and why coverage_24h could not answer this either
coverage_24h is computed from our poll log: it looks for checks that were cut
short. A feed that is never checked writes nothing to that log, so it records no
cut-short check, so the coverage read comes back clean. On a monitor whose feed
was never checked at all, that field was therefore identical to a healthy
monitor's. Zero evidence of loss is not evidence of no loss, and the second time
you make that mistake it is inside the field you built to stop making it.
stream_liveness reads a different source: the list of feeds we are scheduled to
check, and the timestamp we stamp on each one every time a check succeeds.
Neither can be produced by a check that never ran.
status | Meaning |
|---|---|
live | Every feed this monitor watches was checked recently, each within three times its own check interval. |
degraded | At least one is not. streams[] names each one and why: never_polled (registered, and not one successful check on record), stalled (checked before, nothing for over three intervals), unregistered (the feed is not on our polling list at all, which is a fault on our side -- contact support quoting the monitor id). |
unknown | We could not establish it. reason says why, including streams_awaiting_first_poll for a monitor created moments ago, which is ordinary and clears itself within a few minutes. |
coverage_24h cannot read complete unless this field reads live. That is
deliberate: a clean poll log means nothing if there were no polls in it.
GET /reddit/monitor/deliveries?id=<monitor_id>
The actual posts a monitor's webhook has received (or attempted), newest
first, not just counts. health answers "how many"; this answers "which
ones, and what were they". Omit id to aggregate delivery history across
every monitor on your account instead of one.
{
"deliveries": [
{
"delivery_id": "d_a1b2c3",
"monitor_id": "mon_9f8e7d",
"webhook_id": "wh_5c4b3a",
"status": "delivered",
"attempts": 1,
"last_error": null,
"created_at": "2026-08-11T05:12:03.000Z",
"delivered_at": "2026-08-11T05:12:04.000Z",
"payload": {
"items": [
{
"title": "...",
"subreddit": "SaaS",
"permalink": "/r/SaaS/comments/...",
"enrichment": {
"relevance": { "score": 0.82, "matched_terms": ["saas", "alternative"], "considered_terms": 3, "method": "keyword_match_heuristic" },
"sentiment": { "polarity": -0.6, "label": "negative", "positive_terms": [], "negative_terms": ["broken", "frustrating"], "method": "lexicon_heuristic" },
"intent": { "tag": "complaint", "method": "rule_based_heuristic" }
}
}
]
}
}
]
}Every delivered item carries an enrichment object: relevance.score (0-1,
how much of the monitor's own keyword criteria this item matched, not a
model's confidence), sentiment.polarity (-1 to 1, plus a positive
/negative/mixed/neutral label) and intent.tag (one of
question, recommendation_request, complaint, promotion, praise,
discussion). Each carries its own method field naming exactly how it was
produced. All three are deterministic keyword/lexicon/rule heuristics computed
on our side at no extra cost, not a machine-learning or LLM call, and the
method field says so on every response so a score is never mistaken for
model-grade confidence. A monitor with no keyword criteria (a bare
subreddit/author watch) reports relevance.score: 1 with
method: "no_keyword_criteria", since there is nothing else to check it
against.
Optional status narrows to one delivery state (pending, delivered,
failed, dead, suppressed); limit (1-200, default 50) and before (an
ISO-8601 cursor, the oldest row's created_at from the previous page) page
through history the same way every other listing on this API does.
Webhooks
POST /reddit/monitor/webhook/create
Register a delivery target.
{ "kind": "webhook", "url": "https://example.com/hooks/reddit" }kind is one of webhook, slack, discord, email.
The secret is returned once, in this response only. We store it for signing and cannot show it again. Save it before you close the response.
URLs must be HTTPS and must resolve to a public address. See Webhooks for the full rules, including that the check runs again at delivery time.
GET /reddit/monitor/webhook/list
Your webhooks. Secrets are never included, by design: the list endpoint has no need for them, and an endpoint that returns a secret is one more way for it to end up in a log.
POST /reddit/monitor/webhook/test
Send a synthetic delivery to a registered target, signed exactly like a real one. Use it to verify your signature check before real traffic depends on it.
Returns the receiving endpoint's status code and the delivery id, so a failure tells you what your server actually said rather than only that it did not work.
POST /reddit/monitor/webhook/delete
Remove a target. Monitors delivering only to it stop delivering.
Errors
| Status | Meaning |
|---|---|
| 400 | The filter spec or webhook payload is invalid. The body names the field. |
| 401 | Missing or invalid API key. |
| 402 | Plan limit reached, for example all monitor slots in use. |
| 404 | The monitor or webhook does not exist, or is not yours. |
| 429 | Management rate limit. Retry after the window. |
404 rather than 403 for objects you do not own is deliberate. A 403 confirms that an id exists, which lets someone enumerate other customers' monitors one request at a time. A 404 tells them nothing.
Independent third-party API for developers and researchers. Not affiliated with, endorsed by, or sponsored by Reddit, Inc.
