Developer docs
Webhooks, Slack and DiscordPro and up
Hear about new posts, status changes and comments.
Under Developers, add an endpoint and pick the events. We send a POST with JSON to your server, or a short message to a Slack or Discord channel.
post.createdA post is createdpost.status_changedA post changes status (including when it's merged)comment.createdSomeone comments, or the team replies
Slack and Discord
Pick Slack or Discord and paste the channel's incoming webhook URL. In Slack, create an incoming webhook for the channel; in Discord, channel settings → Integrations → Webhooks → New webhook → Copy URL. Messages link to the post in your dashboard.
Your server
POST /your/endpoint
Content-Type: application/json
FDBK-Event: post.status_changed
FDBK-Delivery: 6592c4b9-7980-4879-94e7-535c15ba782a
FDBK-Signature: t=1791046922,v1=7cb9a0f9…
{
"id": "6592c4b9-7980-4879-94e7-535c15ba782a",
"type": "post.status_changed",
"created_at": "2026-10-03T17:02:00.781Z",
"data": {
"post": { "id": 22, "status": "in_progress", … },
"previous_status": "planned"
}
}comment.created also carries data.comment. Answer any 2xx within 10 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours, then dropped. Redirects aren't followed. An endpoint that fails 20 times in a row is turned off until you turn it back on. Deliveries can arrive out of order and, rarely, twice: use the delivery id to skip repeats. Pro includes 5 endpoints and 20,000 deliveries a month, retries included.
Verifying signatures
Each endpoint has a signing secret (whsec_…). The signature is an HMAC-SHA256 of the timestamp, a dot and the raw body. Compare in constant time and reject old timestamps.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}