> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pivotal.app/llms.txt.
> For full documentation content, see https://docs.pivotal.app/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pivotal.app/_mcp/server.

# Receiving webhooks

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

NODE / EXPRESS

```typescript title="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

```python title="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.