RedditapisRedditapis
Live Monitoring

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:

FieldMeaning
delivered_24hSuccessful deliveries
failed_24hDeliveries that exhausted the retry ladder
suppressed_24hAlerts 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_breakdownWhy 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_reachedWhether 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_atTimestamp 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_24hWhether 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_livenessWhether 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_24h and broken out as suppressed_breakdown.stale,
  • readable individually on /reddit/monitor/deliveries?status=suppressed, with the posts themselves, under payload.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:

statusMeaning
completeEvery feed was checked recently, and every check reached the point where the previous check finished. Nothing was skipped over.
degradedAt 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.
partialNothing 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.
unknownThe 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.

statusMeaning
liveEvery feed this monitor watches was checked recently, each within three times its own check interval.
degradedAt 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).
unknownWe 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

StatusMeaning
400The filter spec or webhook payload is invalid. The body names the field.
401Missing or invalid API key.
402Plan limit reached, for example all monitor slots in use.
404The monitor or webhook does not exist, or is not yours.
429Management 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.

On this page