Guides · Your site

Webhooks

Tell your site when what the delivery API answers has changed, so it can rebuild or revalidate. Events, signed requests (Standard Webhooks), retries and the delivery log.

7 min read

A webhook tells your site that what the delivery API answers has changed, so it can rebuild or revalidate. Add one under Settings → Webhooks, or, in develop and staging, through the agent API (createWebhook). Production webhooks are added in the dashboard only. Like an API token, it belongs to one space and one environment, so a production site only hears about production.

The signing secret (whsec_…) is shown once, when the webhook is added. Keep it server-side: it proves a request came from Obelism Content.

Events

EventSent whendata
entry.publishedAn entry is published (first time or again){ entry }
block.publishedA shared block is published: once, for every live page it updated{ entry, entries }
entry.unpublishedAn entry is taken off the delivery API{ entry }
entry.deletedA published entry is deleted{ entry }
entry.movedAn entry's parent, slug or position changes{ entry, previousPath, previousPaths }
asset.updatedAn asset's alt text, title or focal point changes, or its file is replaced, in each environment whose content uses it{ asset, entries }
model.changedContent types are created, changed or deleted{ contentTypes: [{ apiId, action }] }
locales.changedThe space's locales change (every environment){ defaultLocale, locales }
entry.review_requestedSomeone asks for review in a reviewed environment{ entry, revision, note }
entry.review_closedA review is approved (published) or sent back{ entry, outcome, revision, reason? }
product.updatedA product of the space's Shopify store is created or changed (price, images, title, variants), or a variant goes in or out of stock{ product, collections, entries, tags }
product.deletedA product is deleted in the store{ product, collections, entries, tags }
collection.updatedA collection is created or changed, including products added or removed by hand{ collection, entries, tags }
collection.deletedA collection is deleted in the store{ collection, entries, tags }
webhook.testSend test is pressed in Settings{}

A webhook with no events selected receives all of them. Select content types to hear only about entries of those types: entry events, and asset.updated and model.changed when they involve one of them. Events about no content type (locales.changed, webhook.test) always go out. entry is { id, contentType, name, path, paths }: enough to know what to revalidate, never the content itself; fetch that from the delivery API. path is the default locale's and paths the path in every locale ({ [locale]: path }, see paths per locale); path is null and paths {} for data entries. When an entry moves, everything below previousPath (and, per locale, previousPaths) moved with it. asset.updated names the asset ({ id, filename }) and up to 100 entries that use it, drafts included.

A shared block's publish is one block.published event, not an entry.published per page: entry is the block and entries the live pages it updated. Pages left on the version they show now are absent. A webhook limited to content types hears it when the block's own type is selected; it names the pages in entries so you can rebuild each of them.

Shopify changes

With a Shopify store connected, the store's changes reach your site through the same webhooks, so it can revalidate what it cached of a product or collection: its prices, stock, images and title, the pages that show it, and your feeds. They go to every webhook that selects them, also one limited to some content types, and to every environment.

product and collection are { id, handle } (the GID and the handle). collections are the collections the product is in, whose pages and feeds list it. entries are the published pages whose content names the item or those collections (its product page, collection pages, grids, links), with their paths, up to 100. tags are shopify:product:<number> and shopify:collection:<number>: if your site tags what it fetches from Shopify with them, revalidate by tag and pages Content doesn't know of (a feed, a search) follow too. A deleted product has no collections and its handle is null.

export const POST = createWebhookHandler({
	secret: process.env.CONTENT_WEBHOOK_SECRET!,
	onEvent: (event) => {
		if (event.type === 'product.updated' || event.type === 'product.deleted') {
			for (const tag of event.data.tags) revalidateTag(tag)
			for (const entry of event.data.entries)
				for (const path of Object.values(entry.paths))
					revalidatePath(`/${path}`)
		}
	},
})

Requests

Each event is one POST with a JSON body:

{
	"type": "entry.published",
	"timestamp": "2026-10-13T19:30:00.000Z",
	"space": "space_…",
	"environment": "production",
	"data": {
		"entry": {
			"id": "entry_…",
			"contentType": "page",
			"name": "Hello",
			"path": "blog/hello",
			"paths": { "en": "blog/hello", "nl": "blog/hallo" }
		}
	}
}

Requests follow the Standard Webhooks spec, so its libraries verify them:

HeaderValue
webhook-idThe delivery's id; the same on every retry of that delivery
webhook-timestampSeconds since the epoch, when this attempt was sent
webhook-signaturev1, + base64 HMAC-SHA256 of {id}.{timestamp}.{body}

The HMAC key is the base64-decoded part of the secret after whsec_.

app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import { Webhook } from 'standardwebhooks'

const webhook = new Webhook(process.env.CONTENT_WEBHOOK_SECRET!)

export async function POST(request: Request) {
	const body = await request.text()
	// Throws when the signature or timestamp is wrong.
	webhook.verify(body, Object.fromEntries(request.headers))
	revalidateTag('content')
	return new Response(null, { status: 204 })
}

Reject requests whose timestamp is more than a few minutes old, which the libraries do, and use webhook-id to ignore a delivery you already handled.

With the SDK

With @obelism/content 0.6.0 or later, createWebhookHandler verifies the request and hands you the event, typed per type:

app/api/revalidate/route.ts
import { createWebhookHandler } from '@obelism/content/webhooks'
import { revalidatePath, revalidateTag } from 'next/cache'

export const POST = createWebhookHandler({
	secret: process.env.CONTENT_WEBHOOK_SECRET!,
	onEvent: (event, { id }) => {
		// `id` is the webhook-id: the same on every retry of a delivery.
		if (event.type === 'entry.published' && event.data.entry.path) {
			revalidatePath(`/${event.data.entry.path}`)
		} else {
			revalidateTag('content')
		}
	},
})

It answers 401 to a bad signature or a timestamp more than 5 minutes off (tolerance, in seconds), 204 once onEvent is done, and 500 when it throws, so the delivery is retried. verifyWebhook(request, secret) only verifies and returns the event; see the JavaScript SDK.

On a host without tag revalidation. Oxygen (Shopify) has no tag cache, can't purge the live deployment's cache, and its Cache API is per data centre: a webhook there can warm a page or clear one key where it lands, not everywhere. Content already purges its own edge cache when something is published, so a short TTL on the site covers most of it (Shopify Oxygen).

Delivery and retries

Answer with any 2xx within 10 seconds. Anything else, a redirect included, counts as a failure and is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours and 6 hours: eight attempts over about 11 hours. After that the delivery is marked failed. Order isn't guaranteed; use timestamp or re-read the delivery API when it matters.

Edit changes a webhook's name, URL, events and content types. Pause stops new events being queued for it until you Resume; deliveries already queued still go out, and Send test still works. New secret replaces the signing secret, shown once; the old one stops working at once, so update your site's secret right after.

Settings → Webhooks also lists the latest deliveries with their status, attempts and last error, and can send a finished delivery again (same webhook-id). Deliveries are kept for 30 days. The agent API has the same list (GET …/webhooks/deliveries), test (POST …/webhooks/{id}/test) and redelivery (POST …/webhooks/deliveries/{id}/redeliver), so an agent that sets up a webhook can check it gets a 2xx. Outside production, agents can also edit, pause and rotate webhooks (PATCH …/webhooks/{id}, POST …/webhooks/{id}/rotate-secret).

URLs must use https.

On this page

Marks in the margin

  • Note
  • Watch out
  • Updated

Scribes marked doubtful lines in the margin. That practice is called obelism.

Stuck on a step?