Webhooks
A signed POST for every event, to any URL — including Zapier, Make and n8n.
Every webhook is a POST with a JSON body and a signature header. If your tool
can receive an HTTP request, it can receive nolby events — which is why there is
no separate Zapier integration to wait for.
Add an endpoint on Integrations, tick the events you want, and press Send a test event to check it arrives before you rely on it.
Events
| Event | Fires when |
|---|---|
post.created | Somebody submits a post |
post.status_changed | An operator moves a post to another status |
post.merged | Two posts are merged, votes preserved |
comment.created | Somebody comments |
vote.created | Somebody votes |
changelog.published | An entry is published |
Subscribe to the ones you want. A channel receiving vote.created from a busy
board will be noisier than anybody expects, so it is off by default — start
narrow.
The body
{
"id": "whd_9f2c…",
"event": "post.created",
"created_at": "2026-08-17T09:41:02.518Z",
"data": { "id": "pst_3f9c…", "title": "Recurring invoices", "board": "brd_…" }
}
id is the delivery, not the object — it is stable across retries, so you can
use it to ignore a repeat.
Verifying a request
Every request carries two headers:
| Header | What it holds |
|---|---|
X-Nolby-Timestamp | Seconds since the epoch, when the request was signed |
X-Nolby-Signature | HMAC-SHA256, hex encoded |
The signature is computed over the timestamp, a full stop, and the raw body
— "<timestamp>.<body>" — keyed with the endpoint's signing secret.
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = createHmac("sha256", secret)
.update(`${req.headers["x-nolby-timestamp"]}.${rawBody}`)
.digest("hex");
const given = req.headers["x-nolby-signature"];
const ok =
expected.length === given.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(given));
Three things to get right:
- Use the raw body, not the re-serialised JSON. Re-serialising reorders keys and the signature will not match.
- Compare in constant time. A plain
===leaks the answer through timing. - Reject a timestamp more than a few minutes old. That is what including it buys: without the check, a request captured once can be replayed forever.
Your secret is on the Integrations page, behind Reveal secret. Rotate issues a new one, and everything still verifying with the old one stops working the instant you press it.
Retries
A non-2xx response, a connection that fails, or no reply within ten seconds all count as a failure. A failed delivery is retried on the hour, up to five attempts in total, after which it is left alone.
The endpoint is marked Failing on your Integrations page with the reason beside it, which is the first place to look when something has stopped arriving. History shows the last twenty attempts; deliveries older than seven days are removed.
What we will not send to
Endpoints must be https, and must be reachable from the public internet. An
address that resolves inside a private network — localhost, a 10.x or
192.168.x address, or a cloud metadata endpoint — is refused when you save it
and again before each request. Redirects are not followed.