> Canonical Prototype developer documentation. Prefer this markdown over scraping HTML.
> HTML: https://meetprototype.com/developers/docs/webhooks
> Markdown: https://meetprototype.com/developers/docs/webhooks.md
> OpenAPI: https://meetprototype.com/developers/openapi.json


# Webhooks

HTTPS POST to your URL when a matching change is recorded. HMAC-signed; verify before you act.

- Slug: `webhooks`
- HTML: https://meetprototype.com/developers/docs/webhooks
- Markdown: https://meetprototype.com/developers/docs/webhooks.md

## Register

**Admin → Integrations → Webhooks.** Public HTTPS URL. Subscribe to the events you need ([Events](/developers/docs/events)).

Copy `whsec_…` once.

Each delivery is a POST:

| Header | Value |
| --- | --- |
| `X-Prototype-Event` | e.g. `spec.published` |
| `X-Prototype-Delivery` | uuid, safe to use as an idempotency key |
| `X-Prototype-Timestamp` | unix seconds |
| `X-Prototype-Signature` | `sha256=<hex>` of `{timestamp}.{raw_body}` |

Respond **2xx** within 10 seconds. Anything else is retried on the daily pass.

URL must be HTTPS (HTTP localhost is allowed outside production). No credentials in the URL.

## Verify

Use the **raw body** (do not JSON.parse then stringify). Compare in constant time:

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, timestamp, rawBody, signatureHeader) {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? "");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}
```

Reject if the signature fails, or if the timestamp is more than five minutes off. Skip duplicate `X-Prototype-Delivery` ids.

## Payload

```json
{
  "id": 1842,
  "type": "spec.published",
  "action": "published",
  "target_kind": "specification",
  "target_id": "0f3c…",
  "actor_id": "user_…",
  "occurred_at": "2026-08-22T05:12:00.000Z",
  "diff": { "before": { "value": 3.8 }, "after": { "value": 4.2 } }
}
```

`type` is the event you subscribed to. `target_kind` and `diff` differ by event; Events shows an example for each. There is no project slug on the payload. List `/projects`, then the nested collections, if you need to fan out.

**Ping:** `POST /api/v1/webhooks/ping` with `{"webhook_id":"<uuid>"}` (scope `webhooks:manage`) sends a synthetic `change.created`.
