← Docs

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

EventFires when
post.createdSomebody submits a post
post.status_changedAn operator moves a post to another status
post.mergedTwo posts are merged, votes preserved
comment.createdSomebody comments
vote.createdSomebody votes
changelog.publishedAn 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:

HeaderWhat it holds
X-Nolby-TimestampSeconds since the epoch, when the request was signed
X-Nolby-SignatureHMAC-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.