Receiving webhooks

View as Markdown

Copy a handler, set PIVOTAL_WEBHOOK_SECRET from the endpoint’s signing secret, point Pivotal at the URL.

NODE / EXPRESS
webhook-handler.ts
import express from "express";
import { Webhook } from "svix";
const app = express();
const wh = new Webhook(process.env.PIVOTAL_WEBHOOK_SECRET!);
app.post("/webhooks/pivotal", express.raw({ type: "application/json" }), (req, res) => {
try {
const evt = wh.verify(req.body, {
"svix-id": req.header("svix-id")!,
"svix-timestamp": req.header("svix-timestamp")!,
"svix-signature": req.header("svix-signature")!,
}) as { id: string; type: string; data: { object: any } };
// Idempotent: skip if you've already processed evt.id
handle(evt);
res.status(200).send("ok");
} catch {
res.status(401).send("invalid signature");
}
});
PYTHON / FASTAPI
webhook_handler.py
from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError
import os
app = FastAPI()
wh = Webhook(os.environ["PIVOTAL_WEBHOOK_SECRET"])
@app.post("/webhooks/pivotal")
async def receive(request: Request):
body = await request.body()
try:
evt = wh.verify(body, {
"svix-id": request.headers["svix-id"],
"svix-timestamp": request.headers["svix-timestamp"],
"svix-signature": request.headers["svix-signature"],
})
except WebhookVerificationError:
raise HTTPException(401, "invalid signature")
handle(evt)
return "ok"
RULES
  • Return a 2xx within 10 seconds. Anything else triggers a retry.
  • Read the raw body before parsing JSON. Signature verification fails on re-stringified payloads.
  • Idempotency: every retry carries the same id. Store processed ids for at least 7 days.
  • Order is best-effort, not guaranteed. Prefer reading state from data.object over inferring from event order.