RedditapisRedditapis
Live Monitoring

Webhooks

Verify the HMAC-SHA256 signature, deduplicate on the delivery id, and understand the retry ladder before a delivery reaches you.

Every match is delivered as an HTTPS POST to the endpoint you registered.

Headers

HeaderMeaning
x-redditapis-signaturet=<unix_seconds>,v1=<hex hmac>
x-redditapis-timestampThe same unix timestamp, for convenience
x-redditapis-delivery-idStable id for this delivery. Deduplicate on this.

Verifying the signature

The signed string is the timestamp, a literal dot, then the raw request body:

HMAC-SHA256(secret, `${timestamp}.${rawBody}`)

Sign the bytes you received, not a re-serialised object. JSON.parse followed by JSON.stringify can reorder keys or change number formatting, and the signature will then fail for reasons that look like a wrong secret.

const crypto = require("node:crypto");

function verify(secret, header, rawBody, toleranceSeconds = 300) {
  // header looks like: t=1786290000,v1=ab12...
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("="))
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t)) return false;

  // Reject stale timestamps, or a captured request can be replayed forever.
  const age = Math.abs(Math.floor(Date.now() / 1000) - t);
  if (age > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  // Constant-time compare. A plain === leaks timing information about how much
  // of the signature matched, which is enough to forge one byte at a time.
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1 || "", "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hmac, hashlib, time

def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False

    if abs(int(time.time()) - t) > tolerance:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{t}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, parts.get("v1", ""))

The tolerance window is 300 seconds. Anything older is a replay.

Retries

A delivery is retried when the attempt is worth repeating: a network failure, a 5xx, or a 429. Other 4xx responses are not retried, because a 400 or a 404 means the request will fail again and hammering someone's server with our name on it is abuse rather than persistence.

The ladder is front-loaded so a brief blip recovers quickly while a long outage backs off, and jittered so every customer's retries do not land on the same second after a shared incident:

AttemptDelay before it
1immediate
25 seconds
330 seconds
42 minutes
55 minutes
613 minutes

Six attempts over roughly 21 minutes. After that the delivery is dead-lettered and visible on the monitor's health endpoint.

Respond 2xx as soon as you have durably accepted the payload, and do the work afterwards. Holding the connection open while you process makes slow work look like a failure and earns you a retry you did not need.

Secrets

The secret is shown once, when you create the webhook. We store it for signing and cannot show it again. Rotate it with POST /reddit/monitor/webhook/create and delete the old target, which lets you run both briefly during a cutover.

URL rules

Registered URLs must be HTTPS and must resolve to a public address. Private ranges, loopback, link-local and cloud metadata addresses are rejected.

This is checked at delivery time, not only at registration, because a hostname that resolves publicly when you register it can resolve privately later. A URL that passes once is not trusted forever.

Slack and Discord

Register a webhook with kind set to slack or discord and paste the incoming-webhook URL from that platform. The payload is shaped for that service instead of our JSON envelope, so no receiving code is needed. Signature headers do not apply, since the platform authenticates the URL itself.

Independent third-party API for developers and researchers. Not affiliated with, endorsed by, or sponsored by Reddit, Inc.

On this page