Graphdocs

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

EventSent when
entry.createdAn entry is created, including by import, duplicate or the API.
entry.updatedAn entry's title, slug, fields or status change.
entry.publishedAn entry goes live.
entry.unpublishedA published entry is taken down or archived.
entry.deletedAn entry is deleted.
content_type.createdA content type is created.
content_type.updatedA content type or its fields change.
content_type.deletedA content type is deleted. Its entries go with it; they don't each send entry.deleted.
staging.pushedStaging changes are pushed to production. Sent to production endpoints, with the list of what changed.
pingYou 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:

HeaderValue
Content-Typeapplication/json
User-AgentGraph-Webhooks/1.0
X-Graph-EventThe event, e.g. entry.published
X-Graph-Event-IdThe event's id. It's the same on retries, so use it to skip duplicates.
X-Graph-Signaturet=<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:

verify.js
1import { createHmac, timingSafeEqual } from 'node:crypto'
2
3export 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)) <= toleranceSeconds
7 return fresh && expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
8}
verify.py
1import hashlib, hmac, time
2
3def 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"])) <= tolerance
7 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.