Purging from a CMS webhook
Goal: product descriptions live in a headless CMS. When an editor publishes a change, the product pages that show it are cleared within seconds, without waiting for their lifetime to run out.
A publish in the CMS never passes through GraphPilot, so automatic invalidation does not see it. The CMS has to say what changed, and a small handler turns that into a purge.
The running example is shop-api. Product carries @surrogateKey(of: "id"), so every cached
response that shows product p01 is tagged Product:id:p01. The CMS stores each product’s copy
under the same id and calls a webhook on every publish with a body such as
{"entry": {"productId": "p01"}}.
What you need
Section titled “What you need”- An organization API key with the purge permission and nothing else, created in the portal under
Settings → API keys (see API keys and permissions).
Store it where the handler runs, for example as
GRAPHPILOT_API_KEY. - A place to run the handler: a serverless function, or a route in an app you already run.
- A CMS that can call a webhook on publish.
1. Find the key the entries carry
Section titled “1. Find the key the entries carry”Read the key off a real response rather than building it by hand. With
response.surrogate_key_header switched on:
curl -sS -D- -o /dev/null "https://$GRAPHPILOT_HOST/graphql" \ -H 'content-type: application/json' \ -d '{"query":"query ProductPage { product(id: \"p01\") { id name } }"}' \ | grep -i '^gp-surrogate-key:'gp-surrogate-key: ck:… q:product Product Query catalogue Product:category.id:audio Product:id:p01Product:id:p01 is the key to purge when the copy of p01 changes.
2. Write the handler
Section titled “2. Write the handler”export async function handlePublish(request: Request): Promise<Response> { // Check the CMS's webhook signature here, before anything else. const { entry } = await request.json();
const response = await fetch("https://api.graphpilot.io/purge", { method: "POST", headers: { authorization: `Bearer ${process.env.GRAPHPILOT_API_KEY}`, "content-type": "application/json", }, body: JSON.stringify({ serviceId: "shop-api", keys: [`Product:id:${entry.productId}`], soft: true, }), });
// Let the CMS retry on a failure: purging the same key again is safe. return new Response(null, { status: response.ok ? 204 : 502 });}serviceId takes the service’s subdomain. soft: true marks the entries stale instead of
removing them: with the stale window Product has, readers keep
getting the old copy for the moment it takes to fetch the new one. Use a hard purge (leave soft
out) when the old copy must not be shown again, such as a wrong price.
A purge that ran answers 200 with the keys it covered:
{ "success": true, "purgedKeyCount": 1, "purgedKeys": ["Product:id:p01"] }purgedKeyCount counts keys, not cached responses: a key nothing was stored under still counts.
A refused purge answers with a status that matches its extensions.code; the list is under
over REST.
3. Publish and check
Section titled “3. Publish and check”Publish a change to p01 in the CMS, then ask for the product page again:
curl -sS -D- -o /dev/null "https://$GRAPHPILOT_HOST/graphql" \ -H 'content-type: application/json' \ -d '{"query":"query ProductPage { product(id: \"p01\") { id name } }"}' \ | grep -i '^gp-cache:'After a soft purge the first answer is gp-cache: STALE, served from the old copy while
GraphPilot fetches the new one; the next is a HIT with the new copy. After a hard purge it is a
MISS.
4. Find it in the portal
Section titled “4. Find it in the portal”The service’s Purges page lists the purge with the trigger Manual and
the outcome Marked 1 key stale. Actor is empty, because an API key sent it. To find the
purges of one product, filter by Key with Product:id:p01. A purge takes effect at once and
can take up to a minute to appear in the list.
Pitfalls
Section titled “Pitfalls”- One product’s key does not reach everything that mentions it. A purge matches keys exactly,
type name included. A cached review list tagged
Review:productId:p01is not reached byProduct:id:p01; name both when the CMS change affects both. - A new product does not appear in a cached list on its own. No cached list contains it yet, so its key clears nothing. Purge a key the list carries as well; see lists and creates.
- Bulk publishes need batching. One call takes at most 1000 keys. Split a larger import into several calls rather than purging everything.
- No soft purge for everything.
{"all": true}is always hard and sends every next request to your origin. Prefer keys.