Examples
Webhook receiver
A minimal Express server that verifies Nimply webhook signatures.
Scopes: webhooks:manage (to register the endpoint).
This recipe builds the smallest correct receiver: raw-body capture, X-Nimply-Signature
verification, fast 2xx acknowledgement, and deduplication. The full event catalogue and
retry behavior live in the webhooks guide.
1. The server
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
const SECRET = process.env.NIMPLY_WEBHOOK_SECRET; // whsec_..., returned on webhook creation
function verify(rawBody, header, secret) {
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
const seen = new Set(); // use a persistent store in production
app.post("/hooks/nimply", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.get("X-Nimply-Signature");
if (!signature || !verify(req.body, signature, SECRET)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body);
// Deliveries can repeat — dedupe by the envelope id
if (seen.has(event.id)) return res.sendStatus(200);
seen.add(event.id);
// Acknowledge fast, process after
res.sendStatus(200);
switch (event.type) {
case "post.published":
console.log("live:", event.data.postId, event.data.postUrl);
break;
case "post.failed":
console.error("failed:", event.data.postId, event.data.error);
break;
default:
console.log("event:", event.type);
}
});
app.listen(3000);Two details are load-bearing:
express.raw, notexpress.json— thev1value is the hex HMAC-SHA256 of"{t}.{rawBody}"using your secret. Parsing and re-serializing the JSON can change key order and break the signature, so the HMAC must be computed over the raw bytes.- Timestamp check — rejecting
tolder than ~5 minutes prevents replay of captured deliveries. Compare withtimingSafeEqual, never===.
2. Register the endpoint
curl -X POST https://api.nimply.io/v1/webhooks \
-H "Authorization: Bearer $NIMPLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Publishing monitor",
"url": "https://example.com/hooks/nimply",
"events": ["post.published", "post.failed"]
}'The response includes the signing secret (whsec_...) — store it as
NIMPLY_WEBHOOK_SECRET. It is per-webhook, not per-workspace.
3. Test it
curl -X POST https://api.nimply.io/v1/webhooks/{webhookId}/test \
-H "Authorization: Bearer $NIMPLY_API_KEY"Your server should log a webhook.test event and return 200. If it logs
"invalid signature" instead, the usual culprit is a JSON body parser mounted before the
raw handler.
Gotchas
- Respond
2xxwithin 15 seconds — do slow work after acknowledging, not before. - Failed deliveries are retried 5 times with exponential backoff; after 10 consecutive failures the endpoint is auto-disabled (
isActive: false). Re-enable withPATCH /v1/webhooks/{id}once fixed. - Deliveries can arrive out of order and more than once — the
X-Nimply-Deliveryheader (the event id) is stable across retries, so dedupe on it or on the envelopeid.