Webhook payload
One POST per match, JSON body, within minutes of the post or comment appearing on Reddit. The same object comes back from GET /api/v1/alerts, so you only learn one shape.
Headers
| Header | Value |
| --- | --- |
| Content-Type | application/json |
| User-Agent | Redship-Webhook/1.0 |
| X-Webhook-ID | The webhook's id |
| X-Webhook-Event | post, comment or test |
| X-Webhook-Signature | sha256=<hex HMAC-SHA256 of the raw body, keyed with your secret> |
Post
event: post
{
"event": "post",
"timestamp": "2026-08-26T09:41:12+00:00",
"webhook_id": "8d1f…",
"website": null,
"inbox_id": "c0a8…",
"id": "1n3x9kq",
"title": "Looking for a Notion alternative for a small team",
"subreddit": "productivity",
"author": "throwaway_pm",
"selftext": "We are five people and Notion got slow…",
"url": "https://www.reddit.com/r/productivity/comments/1n3x9kq/…",
"keyword": "notion alternative",
"keyword_id": "5b2c…",
"relevance_score": null,
"upvotes": 3,
"replies": 0,
"created_utc": 1756201272
}Comment
event: comment
{
"event": "comment",
"timestamp": "2026-08-26T09:41:12+00:00",
"webhook_id": "8d1f…",
"website": null,
"inbox_id": "c0a8…",
"id": "nb7x2ka",
"subreddit": "SaaS",
"author": "founder_jane",
"comment": "We moved off Stripe webhooks to polling because…",
"url": "https://www.reddit.com/r/SaaS/comments/…/nb7x2ka/",
"keyword": "stripe webhook",
"keyword_id": "9e0d…",
"relevance_score": null,
"comment_upvotes": 1,
"created_utc": 1756201272,
"title": "How do you handle payment failures?",
"selftext": "Parent post text…",
"upvotes": 12,
"replies": 8
}relevance_score and website are always null on the Alerts product. keyword_id is the id from GET /api/v1/keywords.
Verifying the signature
Compute HMAC-SHA256 of the raw request body with the secret you received when creating the webhook, and compare it to the header after the sha256= prefix.
Node.js
import crypto from "node:crypto"
export function verify(rawBody, header, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex")
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header || ""))
}Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")PHP
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
$ok = hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '');Sign the raw bytes, not a re-serialized object: any change in key order or whitespace breaks the comparison.
Retries and timeouts
Your endpoint has 10 seconds to answer with a 2xx. There is no automatic retry: a failed delivery is recorded on the webhook (last_error, consecutive_failures, visible in GET /api/v1/webhooks) and the alert stays available from GET /api/v1/alerts. Answer fast and process later.