Features
Webhooks
Events sent to your endpoints when content changes.
Webhooks tell your systems when content changes in Graph: rebuild a static site when something is published, clear a cache, update a search index. Add endpoints under Settings → Webhooks. Each endpoint chooses which events it receives, in which environments (production, staging or both), and optionally for which content types.
Events
| Event | Sent when |
|---|---|
entry.created | An entry is created, including by import, duplicate or the API. |
entry.updated | An entry's title, slug, fields or status change. |
entry.published | An entry goes live. |
entry.unpublished | A published entry is taken down or archived. |
entry.deleted | An entry is deleted. |
content_type.created | A content type is created. |
content_type.updated | A content type or its fields change. |
content_type.deleted | A content type is deleted. Its entries go with it; they don't each send entry.deleted. |
staging.pushed | Staging changes are pushed to production. Sent to production endpoints, with the list of what changed. |
ping | You choose Send test event on an endpoint. |
Publishing an entry sends both entry.updated and entry.published, so subscribe to the ones you need. A site that rebuilds on publish usually listens to entry.published, entry.unpublished, entry.deleted and staging.pushed.
The request
Each event is a POST with a JSON body:
{ "id": "evt_b3JpZ2luYWwx", "event": "entry.published", "createdAt": "2026-10-14T18:02:11.000Z", "environment": "production", "data": { "entry": { "id": "d4fsmgijxvlb", "uid": "d4fsmgijxvlb", "title": "Release notes for October", "slug": "release-notes-october", "status": "published", "language": "en-US", "contentType": { "id": "p8d2k4m9x0s1", "slug": "blog-posts", "name": "Blog posts" }, "fields": { "excerpt": "What shipped this month.", "body": "<p>Lots.</p>" }, "publishedAt": "2026-10-14T18:02:10.000Z", "createdAt": "2026-10-09T09:12:00.000Z", "updatedAt": "2026-10-14T18:02:10.000Z" } }}Entry events carry the entry as the API returns it, fields included (entry.deleted carries it as it was). Content type events carry data.contentType with its fields' names, slugs and types. staging.pushed carries data.changes, each { kind, uid, label, typeName, op }.
Every request has these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Graph-Webhooks/1.0 |
X-Graph-Event | The event, e.g. entry.published |
X-Graph-Event-Id | The event's id. It's the same on retries, so use it to skip duplicates. |
X-Graph-Signature | t=<unix seconds>,v1=<signature>; see below |
Answer with any 2xx status within 10 seconds. Do slow work after responding.
Verifying signatures
Each endpoint has a signing secret (whsec_…), shown on its settings page. v1 is the hex HMAC-SHA256 of <t>.<raw body> using that secret. Check it against the raw request body, before parsing, and reject old timestamps to stop replays:
1import { createHmac, timingSafeEqual } from 'node:crypto'23export function verifyGraphSignature(rawBody, header, secret, toleranceSeconds = 300) {4 const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')))5 const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')6 const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds7 return fresh && expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))8}1import hashlib, hmac, time23def verify_graph_signature(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:4 parts = dict(part.split("=", 1) for part in header.split(","))5 expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()6 fresh = abs(time.time() - int(parts["t"])) <= tolerance7 return fresh and hmac.compare_digest(expected, parts.get("v1", ""))Roll secret on the endpoint's page replaces the secret straight away.
Retries and history
A delivery that doesn't get a 2xx (an error status, a timeout or no connection) is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then marked failed. Each endpoint's page lists its recent deliveries with the request, the response and how long it took. Open one to see the details, or Resend it. Deliveries are kept for 30 days.
Webhook URLs must use https:// and point to the public internet; private and internal addresses are refused.