Webhooks vs. API: a webhook pushes, an API waits to be asked
An API only returns data when your app asks for it. A webhook sends your app a request the moment content changes. No polling loop needed. Draftbase supports both. Read through the delivery API. React through webhooks. You're never stuck choosing one over the other.
Updated
- Entry published
- Webhook queued
- Delivered (200 OK)
Webhooks vs. APIs: what's the difference
An API is pull-based. Your app decides when to ask. It sends a request and gets back the current state. Nothing arrives until you ask for it.
Say you want to know about a change the instant it happens. You have to keep asking. That's called polling. Polling works, but it has a cost. Every poll that finds no change is a wasted request. The delay before you notice a real change is always at least as long as your poll gap.
A webhook flips the direction. The CMS tells your app the instant something changes. Not the other way around. You register a URL and a set of events you care about. Draftbase sends a request to that URL the moment a matching event fires. No interval to tune. No idle loop to run.
Webhooks don't replace the API. They remove the need to poll for writes. You still need an API for reads. A webhook only tells you that something changed. It doesn't hand you the full state of your system. For that, you go back to the API.
Here's a real example. Say you cache a rendered blog post at the edge. Without webhooks, you have two options. Set a short cache time and poll for fresh data. Or accept stale content until the cache expires on its own. With a webhook on entry.published, you clear that one cache entry the moment the edit goes live. Nothing to guess at, nothing to poll.
How Draftbase implements webhooks
Webhook dispatch runs after a write already succeeded. Not as part of it. When an entry is created, updated, or changes status, Draftbase checks which webhooks are subscribed. It matches on environment and template. Then it sends each match a signed request. If a subscriber is slow or offline, the write itself is unaffected. Dispatch runs off to the side. Failures get logged, not shown to the editor.
Nine entry events
Subscribe to entry.created, entry.updated, entry.status_changed, entry.published, entry.unpublished, entry.archived, entry.deleted, entry.rolled_back, or entry.tags_updated.
HMAC-signed deliveries
Every payload is signed with createHmac using a per-webhook secret, so your endpoint can verify it actually came from Draftbase.
Backoff retries with a delivery log
Delivery runs off your publish flow entirely, so a slow endpoint never delays it. A failed delivery retries up to 10 times over 7 days. Every attempt shows up in a delivery log you can check and retry by hand.
CRUD-managed subscriptions
Webhooks are created, updated, and deleted through their own management endpoints, kept separate from the read-only delivery API.
This split mirrors the same design as the rest of Draftbase. Write and read are separate surfaces. Wiring up a webhook never touches your read path. Revoking a webhook never affects your delivery API keys.
A webhook can also scope itself to one environment or one template. That matters once a project has more than one team publishing content. A Slack webhook for marketing doesn't need to fire on every product update too.
Every delivery carries an event ID, an event name, and a timestamp. Your endpoint can use the event ID to skip a repeat. That matters if a retry lands after the first attempt already worked, but timed out before Draftbase saw the reply.
Entry payloads can include the before-and-after values for the change. Not just a bare notice. That's a setting per webhook. Turn it on if your endpoint needs the actual diff. Leave it off to keep the payload small.
What a webhook payload looks like
Every delivery is one JSON envelope. Four fields at the top, then an event-specific payload. Here is an entry.status_changed body for a post going live:
{
"id": "0f2c6a1e-8d44-4b19-9a7e-3c51b0d2f8aa",
"schemaVersion": 1,
"event": "entry.status_changed",
"occurredAt": "2026-08-11T14:03:22.481Z",
"payload": {
"entryId": "66b4f1c9a2d4e10c7f8b3d21",
"entry": {
"id": "66b4f1c9a2d4e10c7f8b3d21",
"orgId": "66a01e5d7c9b4412ab0f5e08",
"envId": "production",
"templateId": "blogPost",
"locale": "en",
"fields": { "title": "Signing webhooks", "slug": "signing-webhooks" },
"tags": ["engineering"],
"status": "published",
"version": 7,
"createdAt": "2026-08-04T09:12:44.002Z",
"updatedAt": "2026-08-11T14:03:22.377Z"
},
"changes": [{ "path": "status", "before": "draft", "after": "published" }]
}
}The entry object and the values inside changes only appear when that webhook has content included turned on. With it off, you still get entryId, the event name, and the list of changed paths. Enough to invalidate a cache without shipping content to a third party.
How to verify a webhook signature in Node
Draftbase sends X-Draftbase-Signature-256 as sha256= plus the hex HMAC of timestamp.rawBody, with X-Draftbase-Timestamp carrying that same Unix timestamp in seconds. Recompute it over the raw bytes, before any JSON parsing, and compare in constant time:
import { createHmac, timingSafeEqual } from 'node:crypto';
const MAX_SKEW_SECONDS = 300;
export function isValidDelivery(rawBody: string, headers: Headers, secret: string) {
const timestamp = headers.get('x-draftbase-timestamp') ?? '';
const received = headers.get('x-draftbase-signature-256') ?? '';
const expected =
'sha256=' +
createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, and === would leak timing.
if (a.length !== b.length || !timingSafeEqual(a, b)) return false;
const skew = Math.abs(Date.now() / 1000 - Number(timestamp));
return Number.isFinite(skew) && skew < MAX_SKEW_SECONDS;
}Two details decide whether this works. Your framework has to hand you the raw body, since re-serializing parsed JSON changes the bytes and breaks the digest. And the comparison stays constant-time, because a === on a signature leaks how many leading characters an attacker guessed right.
Webhooks vs. API polling
The table below lines up three setups on the same four points. Polling the API on a timer. A generic CMS's webhooks. And Draftbase's. The gap between polling and webhooks is the biggest jump. The gap between a generic webhook and a Draftbase one is mostly about defaults.
| Approach | Polling the API | Generic CMS webhooks | Draftbase webhooks |
|---|---|---|---|
| Latency | Delay = poll interval | Near-instant | Near-instant |
| Server load | Constant requests, mostly empty | One request per real change | One signed request per real change |
| Payload authenticity | N/A | Varies by vendor | HMAC-signed, verifiable |
| Setup | Cron job + diffing logic | Webhook URL + event picker | Webhook URL + event picker, same pattern |
The setup row hides how much the real work changes. A polling script has to fetch, check against the last known state, and store that state somewhere. A webhook keeps no state on your side. Draftbase already knows what changed. It tells you directly. You don't have to work it out yourself.
The server-load row also grows at scale in a way the table can't show. A poll gap that's fine for one app becomes real load once ten apps poll the same API on their own clock. Most polls find nothing new, most of the time. A webhook only fires when there's something to say. That cost doesn't grow the same way as the number of listeners grows.
Webhooks vs. WebSockets vs. SSE
A webhook is a one-off HTTP request from server to server: push once, deliver the event, done. A WebSocket is a connection both sides hold open and can write to for as long as the session lasts. SSE is a one-way stream the server pushes down an open HTTP response. Webhook vs. WebSocket comes down to that difference in shape — a single event notification versus a live, continuous connection — and it's why websockets vs. webhooks isn't really a choice between two ways to do the same job. All three deliver updates without polling. They fail in different places, which is what decides between them.
| Property | Webhook | API polling | WebSocket | SSE |
|---|---|---|---|---|
| Direction | Server to server | Client asks, server answers | Both ways | Server to client only |
| Connection lifetime | One request per event, then closed | One request per poll | Held open per client, for as long as the session lasts | Held open per client, one HTTP response |
| Delivery guarantee | At-least-once, retried on failure | Whatever state exists when you ask | None. Messages sent while disconnected are gone | Resumable via Last-Event-ID, if the server implements it |
| Who reconnects | Sender. Draftbase retries for up to 7 days | Caller, on its own timer | You do. Reconnect and backoff are your code | The browser, automatically |
| Typical use | Rebuilds, cache purges, backend sync | Scheduled imports, reconciliation | Collaborative editing, chat, multiplayer cursors | Live progress bars, dashboards, token streaming |
A CMS integration almost always wants the webhook. The consumer is your build server or your cache, not a person watching a screen, and it needs the event to survive a deploy that took the listener down for 40 seconds. A webhook does that, because the sender owns retries. A socket doesn't, because a dropped connection drops whatever was in flight.
So Draftbase ships webhooks and no socket. The one job a WebSocket wins outright is many clients watching the same document at once, which is a collaborative editing feature, not a content delivery one. If you want that shape in your own app, subscribe to webhooks on your server and fan out to browsers over your own socket or SSE stream. Webhooks vs. APIs walks the pull-versus-push tradeoff in more depth.
Webhook security best practices
A webhook endpoint is a public URL that accepts writes from the internet. Six habits keep that from being a problem. The first one is not optional.
- Verify the signature before you parse the body. An unverified payload is anonymous input. Check the HMAC over the raw bytes first, then decide whether to read it.
- Assume the URL is known. URLs leak through logs, proxies, and screenshots. A secret path is not authentication.
- Reject stale timestamps. A valid signature stays valid forever, so a captured request can be replayed. A five-minute window on
X-Draftbase-Timestampcloses that. The Standard Webhooks spec asks for the same check on itswebhook-timestampheader. It signsid.timestamp.payloadwith HMAC-SHA256, so the timestamp sits inside the signed bytes. Outside them, it could be edited in transit. (Source: Standard Webhooks specification) - Respond fast, work later. Each attempt gets a 10-second timeout. Return 2xx once the payload is verified and queued, and do the rebuild or reindex outside the request.
- Be idempotent. Delivery is at-least-once, so the same event will arrive twice sooner or later. Deduplicate on
X-Draftbase-Event-Idand treat a repeat as a no-op. - Don't treat payload contents as permission. A body that names an org or a user proves nothing on its own. The signature is the only thing that says where the request came from.
The idempotency point is the one that bites teams in production. A retry fires whenever Draftbase doesn't see a 2xx, including when your handler finished the work and then timed out on the reply. That delivery succeeded from your side and failed from ours, so it comes back. A handler that posts to Slack without a dedupe check posts twice.
Common webhook use cases
Almost every webhook in a content stack does one of five things. Each one is a reaction to a change, which is why none of them need the full entry in the payload.
- Trigger a rebuild or deploy on
entry.published, so a static site ships the edit instead of waiting for the next scheduled build. - Purge one cache key, not the whole zone. The event names the entry that changed.
- Sync a search index. Upsert on publish, delete on
entry.deleted. - Post to Slack when something goes live.
- Push into a downstream system: a CRM, an inventory service, a mobile push queue.
The rebuild case is the most common by a distance, and it's also where scoping pays off. A webhook filtered to one template means a product edit doesn't rebuild the blog. What a webhook is covers the request mechanics if you're wiring your first one.
When to use webhooks vs. the delivery API
Use the delivery API for reads. When a page loads, pull the current state directly. That's what the API is for. It's the only source that gives a full, correct snapshot at that exact moment. A webhook is a point-in-time event, not a live view.
Use webhooks to react to change. Common cases: clear a cache, update search, post a Slack alert, or kick off a build. In every one of those cases, you don't need the full entry on every event. You just need to know something changed.
Most real setups run both. They pull state on render through the delivery API, and react to change in the background through a webhook. Neither one alone covers both jobs well.
A site rebuild is the clearest case of this. The build pulls every entry it needs through the delivery API. That's a read. It wants one full, correct snapshot. What triggers the build is a webhook on entry.published. It fires the build the moment an editor ships a change. Not a rebuild on a fixed timer.
Why the push model is winning
API-first design is now the default for how teams build. Not the exception. 83.2% of teams now call themselves API-first. That's up from 74% in 2024. (Source) That shift makes webhooks a default choice, not a bolt-on.
The webhook side of that shift is well underway too. Especially at big firms running many linked systems. 78% of Fortune 500 firms now run setups where webhooks are a main pattern. That's up from about 51% in 2021. (Source) Here, that means an API for reads and webhooks for change alerts, running side by side. Neither one replaces the other.
Draftbase's webhooks follow that same pattern by default. The delivery API and the webhook stream are built as one system. Not two products stitched together after the fact.
Neither stat is specific to content tools. Both describe patterns broadly. But the trend fits here too. Teams now expect a content tool to notify them of change. Not make them build a polling layer just to stay in sync.
Vendor lock-in here is low. A webhook is a plain signed request. Switching CMS vendors later doesn't mean rewriting your core logic — only the URL you register and the header you check.
Try Draftbase webhooks
Publish an entry, watch a signed delivery fire, and wire it into your own endpoint. No polling loop, no diffing logic.
Hobby is free, no card. Startup is $49/mo when you outgrow it. The price is on the pricing page, where prices go.
No migration quarter, no kickoff workshop. Define a template and ship something today.
Frequently asked questions
What's the difference between a webhook and an API?
An API is pull-based. Your app sends a request and waits for a response, on your own schedule. A webhook is push-based. The CMS sends your app a request the moment something changes, with no polling required. You get the update as it happens instead of finding it on your next scheduled check. The two aren't competitors — a webhook still arrives as an HTTP request, it's just the CMS initiating it instead of you.
Are Draftbase webhook payloads signed?
Yes. Each delivery is HMAC-signed with SHA-256 using Node's createHmac and a secret unique to that webhook. Your endpoint can recompute the signature from the raw body and compare it, which confirms the payload came from Draftbase and wasn't altered in transit. The signature travels in a request header alongside the event ID and event name, so verification doesn't require parsing the body first.
What events can trigger a webhook?
Nine entry-level events: entry.created, entry.updated, entry.status_changed, entry.published, entry.unpublished, entry.archived, entry.deleted, entry.rolled_back, and entry.tags_updated. Each webhook subscribes to the specific events it cares about, and can optionally scope to one environment or one template, so a single subscription doesn’t have to fire on every event across every project.
How many times does Draftbase retry a failed webhook delivery?
Up to 10 attempts per event, backing off over a span of 7 days. A timeout, a non-2xx response, or a connection error all count as a failed attempt and schedule the next one. Each attempt gets a 10-second timeout. Every attempt is recorded in a per-webhook delivery log in your dashboard, so you can see what failed and why, and manually retry a delivery immediately instead of waiting for the next scheduled attempt.
What is the difference between a webhook and a WebSocket?
A webhook is one HTTP request per event, sent server to server and closed straight after. A WebSocket is a connection both sides hold open and can write to at any time. The practical difference is who owns recovery: a failed webhook is retried by the sender, while messages sent over a dropped WebSocket are gone unless you built replay yourself. Use a webhook for backend reactions like rebuilds and cache purges, and a WebSocket for live collaboration between browsers.
How do I verify a webhook signature?
Recompute the HMAC and compare it in constant time. Read X-Draftbase-Signature-256 and X-Draftbase-Timestamp, compute an HMAC-SHA256 over the timestamp, a dot, and the raw request body using that webhook's secret, prefix the hex digest with sha256=, and compare with crypto.timingSafeEqual rather than ===. Do it on the raw bytes before parsing JSON, and reject timestamps older than about five minutes so a captured request can't be replayed.
What happens if my webhook endpoint is down?
Draftbase retries. A timeout, a connection error, or any non-2xx response counts as a failed attempt, and the event is redelivered on an escalating backoff up to 10 attempts across 7 days. Every attempt lands in the per-webhook delivery log with its status and error, so you can see what failed while you were down and retry a specific delivery by hand once the endpoint is back, instead of waiting for the next scheduled attempt.
Do I still need the delivery API if I use webhooks?
Yes. Webhooks tell you something changed. They are not a data store, and a payload is a snapshot of one event, not a live view of your content. Use the delivery API to pull the current published state when a page renders, and webhooks to react to changes as they happen. Most integrations end up using both, not one instead of the other.