Headless CMS Webhooks Explained: Automate Your Content Pipeline (2026)
Event-driven POSTs, HMAC signatures, and rebuild-on-publish
You hit publish on a blog post. Ten seconds later your static site has already rebuilt, your CDN cache is flushed, your search index is updated, and a message lands in your team's Slack. Nobody clicked a deploy button. That whole chain runs on webhooks.
If you build headless front-ends, webhooks are the glue between your CMS and everything downstream. Without them you're stuck polling an API on a timer, hoping you catch new content before your readers do. With them, your CMS pushes the moment something changes.
TL;DR
A webhook is an event-driven POST request. When something happens in your CMS — a post is published, updated, or deleted — the CMS sends an HTTP POST to a URL you registered. No polling, no waiting. The payload tells your front-end what changed, and a signature proves the request really came from your CMS.
For headless setups, webhooks solve the "how does my static site know new content exists?" problem. You subscribe a Vercel or Netlify build hook to the post.published event, and every publish triggers a fresh deploy automatically.
This post covers what webhooks are, why headless front-ends need them, the anatomy of a webhook (URL, event, payload, signature), how to verify HMAC signatures so you don't process fake requests, and how retries and delivery logs keep things reliable. We'll end with a concrete UnfoldCMS example — subscribing a Vercel deploy hook to publish events and verifying the signature on the receiving end.
If you want the wider picture on how content flows out of a headless CMS, our guide to what a content API is and the deep dive on triggering frontend rebuilds with CMS webhooks both pair well with this one.
What are CMS webhooks?
A CMS webhook is a way for your CMS to notify another system the instant content changes, by sending an HTTP POST to a URL you choose. It's push, not pull. Instead of your app asking "anything new?" over and over, the CMS tells you the moment it happens — with a payload describing the event.
Think of it like a doorbell. Polling is you opening the door every 30 seconds to check if someone's there. A webhook is the bell ringing when they actually arrive. One wastes energy; the other reacts to real events.
Webhooks vs polling: what's the difference?
Polling means your front-end calls the CMS API on a fixed schedule to check for changes. Webhooks flip that — the CMS calls you when something changes. Polling wastes requests and adds delay; webhooks are near-instant and only fire on real events. For headless builds where freshness matters, webhooks win almost every time.
Here's the trade-off side by side:
| Factor | Polling | Webhooks |
|---|---|---|
| Direction | Front-end pulls from CMS | CMS pushes to front-end |
| Freshness | Delayed (up to poll interval) | Near-instant |
| Wasted requests | Many (most return nothing) | Zero (only fires on events) |
| Setup | Simple, no public endpoint | Needs a public receiver URL |
| Security | API token on your side | Signature verification needed |
| Best for | Batch syncs, rate-limited APIs | Rebuilds, cache flush, notifications |
Polling still has its place — background syncs, or when you can't expose a public endpoint. But for reacting to a publish, webhooks are the right tool.
What are CMS webhooks used for?
The most common use is rebuilding a static site on publish, but that's just the start. Teams use webhooks to invalidate CDN caches when content changes, push updates into a search index like Algolia or Meilisearch, notify a Slack or Discord channel, and sync content into other services. Any "when X happens, do Y" flow is a candidate.
Here are the patterns that show up most in headless projects:
- Rebuild a static site. A
post.publishedevent hits your Vercel or Netlify build hook, and the deploy kicks off. Your Astro, Next, or Hugo site regenerates with the new content. - Invalidate cache. When a post updates, ping your CDN or edge cache to purge the stale version so readers see the fresh copy.
- Sync a search index. On publish or update, send the changed record to your search provider so on-site search stays current.
- Notify a channel. Drop a message in Slack when something goes live — handy for editorial teams tracking what shipped.
- Trigger a workflow. Kick off anything downstream: translation jobs, social auto-posts, analytics events.
What's inside a webhook? The anatomy
Every webhook has four parts: the URL (where the POST goes), the event (what happened, like post.published), the payload (a JSON body with the details), and the signature (a hash proving the request is genuine). Your receiver reads the event, verifies the signature, then acts on the payload.
A typical payload looks like this — an event name, a timestamp, and the data that changed:
{
"event": "post.published",
"timestamp": "2026-07-03T09:00:00Z",
"data": {
"id": 412,
"slug": "headless-cms-webhooks-explained",
"title": "Headless CMS Webhooks Explained",
"status": "published"
}
}
The signature usually rides along in an HTTP header, not the body. That keeps it separate from the data it's signing. On the wire, the CMS computes an HMAC hash of the raw payload using a shared secret, and your receiver recomputes the same hash to confirm nothing was tampered with. More on that next.
Why do webhooks need signatures?
Your webhook receiver is a public URL — anyone who finds it can POST to it. Without verification, an attacker could send fake post.published events to trigger endless rebuilds or poison your cache. A signature solves this: the CMS signs each payload with a secret only both sides know, so your receiver can prove the request is authentic before acting.
This matters more than people expect. A rebuild endpoint left unverified is a free denial-of-wallet attack — someone spams it and burns your build minutes. Signature checks are the cheap insurance.
How does HMAC signature verification work?
HMAC (hash-based message authentication code) combines your payload with a shared secret and runs both through a hash like SHA-256. The CMS sends the resulting hash in a header. Your receiver runs the exact same calculation on the raw body it received. If your hash matches the one in the header, the request is genuine and untampered. If not, you reject it.
The key detail: hash the raw request body, byte for byte, before any JSON parsing. Parsing and re-serializing can reorder keys or change whitespace, which breaks the hash. Grab the raw bytes first.
Here's the verification logic in pseudocode:
function verifyWebhook(rawBody, signatureHeader, secret):
expected = hmac_sha256(secret, rawBody) // hex digest
received = signatureHeader // e.g. "sha256=abc123..."
// strip any "sha256=" prefix from received, then compare
if constant_time_equals(expected, received):
return true // genuine — process the event
else:
return false // reject — 401, do nothing
Two things to get right. Use a constant-time comparison (like hash_equals in PHP or crypto.timingSafeEqual in Node), not ==. A normal string compare leaks timing info that can help an attacker guess the hash. Second, if verification fails, return a 401 and do nothing — don't rebuild, don't log it as success.
For the broader auth picture on headless CMS APIs — tokens, scopes, and how signing fits in — see our post on headless CMS authentication and API tokens.
What about retries, idempotency, and delivery logs?
Networks fail. Your receiver might be down for a deploy, or time out. A solid webhook system retries failed deliveries with backoff, keeps a log of every attempt (status code, response, timestamp), and expects your receiver to be idempotent — meaning the same event delivered twice produces the same result, no duplicates.
Idempotency is on you, the receiver. If a webhook fires twice because the first response timed out, your handler should notice it already processed that event ID and skip it. A retried publish shouldn't trigger two deploys or two Slack messages.
Delivery logs are your debugging lifeline. When a rebuild doesn't fire, the log tells you whether the CMS even sent the webhook, what your endpoint returned, and how many retries it took. Without logs you're guessing.
How does UnfoldCMS handle webhooks?
UnfoldCMS ships outgoing webhooks built for exactly this. You create a WebhookSubscription with a target URL, the events you care about, and an HMAC-SHA256 signing secret. When content fires an event — post publish, update, or delete — a DispatchApiWebhooks listener sends a signed POST to every matching subscriber. Each delivery attempt is recorded in a webhook_calls table so you have a full log.
You manage subscriptions through the admin API at /api/v1/admin/webhooks, and there's a test endpoint so you can fire a sample payload and confirm your receiver handles it before going live. The signing secret means every request carries an HMAC-SHA256 signature you verify on your end — no unverified rebuilds.
This slots straight into the rest of the UnfoldCMS v1 API, so the same headless front-end reading your content over REST can also react to it changing.
A concrete example: rebuild Vercel on publish
Let's wire it up. The goal: every time you publish a post, Vercel redeploys your static front-end, and your receiver verifies the request is really from UnfoldCMS.
- Create a Vercel deploy hook. In your Vercel project settings, under Git, create a Deploy Hook. Vercel gives you a URL like
https://api.vercel.com/v1/integrations/deploy/prj_xxx/yyy. Hitting that URL with a POST triggers a build. - Add a webhook subscription in UnfoldCMS. POST to
/api/v1/admin/webhookswith the target URL, thepost.publishedevent, and let it generate an HMAC-SHA256 signing secret. Save that secret somewhere safe. - Fire the test endpoint. Use the built-in test to send a sample payload to your URL. Confirm it arrives and the build kicks off.
- Verify the signature on receive. If you point the webhook at your own small proxy (instead of Vercel directly), verify the HMAC there before forwarding to the Vercel hook. Reject anything that fails.
- Publish a post. The
DispatchApiWebhookslistener sends the signed POST, Vercel rebuilds, and your site goes live with the new content. Check thewebhook_callslog to confirm a 200.
If you go the proxy route, a Node receiver checking the signature looks roughly like:
import crypto from "crypto";
app.post("/webhook", (req, res) => {
const raw = req.rawBody; // raw bytes, not parsed JSON
const sig = req.headers["x-signature"];
const secret = process.env.WEBHOOK_SECRET;
const expected = crypto
.createHmac("sha256", secret)
.update(raw)
.digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig)
);
if (!ok) return res.status(401).send("bad signature");
// genuine — forward to the Vercel deploy hook
fetch(process.env.VERCEL_HOOK_URL, { method: "POST" });
res.status(200).send("ok");
});
That's the whole loop: publish → signed POST → verify → rebuild. No manual deploys, and nothing acts on an unverified request.
Deciding between REST and GraphQL for the content side of your front-end? Our API-first CMS: REST vs GraphQL breakdown covers that choice.
FAQ
Do I need a public URL to receive webhooks? Yes. The CMS sends an HTTP POST, so your receiver has to be reachable on the public internet. For local development, use a tunnel like ngrok or cloudflared to expose your local port temporarily.
What happens if my receiver is down when a webhook fires?
A good webhook system retries with backoff and logs each attempt. UnfoldCMS records every delivery in the webhook_calls table, so you can see what failed and when. Make your handler idempotent so retries don't cause duplicate work.
Can I subscribe to more than one event? Yes. A single subscription can listen for multiple events — publish, update, and delete — or you can create separate subscriptions per event and route them to different URLs.
How is a webhook different from an API call? An API call is something you initiate to fetch or send data. A webhook is initiated by the CMS, pushing data to you when an event happens. One is pull, the other is push.
Does UnfoldCMS support incoming content webhooks? UnfoldCMS ships outgoing webhooks — it notifies your systems when content changes. Incoming webhooks beyond payment events aren't part of the current setup. For pulling content in, use the content API.
Wrapping up
Webhooks turn your headless CMS from a passive data store into an active part of your pipeline. Publish a post, and rebuilds, cache flushes, search updates, and notifications all fire on their own. The two rules that keep it safe and reliable: verify every HMAC signature with a constant-time compare, and make your receiver idempotent so retries don't double up.
UnfoldCMS gives you signed outgoing webhooks, per-subscription secrets, a delivery log, and a test endpoint — enough to wire a Vercel or Netlify rebuild to your publish button in an afternoon. See what else the platform ships on the features page.
Sources: UnfoldCMS admin API (/api/v1/admin/webhooks), Vercel Deploy Hooks documentation, and standard HMAC-SHA256 signing practice (RFC 2104). Code samples are illustrative — adapt header names and secrets to your setup.
Free & Open Source
Own your CMS. No subscriptions.
Unfold CMS is free to download and self-host. Built on Laravel + React, full source code included.
Share this post: