Teams that run Webflow at scale eventually ask the same question: can we stop a publish from happening unless it meets our criteria?
The criteria vary -- a legal sign-off, a content freeze window, a link checker, required SEO fields, a QA pass on staging, an external system that says "not yet." But the shape of the request is always the same: something should stand between the Publish button and the live site.
This article covers what Webflow actually supports, why the obvious solution doesn't exist, and the architectures that do work. It's split into site publishing, page-level publishing, and CMS item publishing, because Webflow treats these as three independent mechanisms with different rules -- and the gating story is meaningfully different for each.
The approach that doesn't work: pre-publish webhook approval
What people want
The intuitive design is a synchronous veto:
- A user clicks Publish in the Designer.
- Webflow calls your server before doing anything.
- Your server evaluates its criteria.
- Your server returns approve or reject.
- Webflow either proceeds or aborts with a message to the user.
This is a well-established pattern elsewhere -- GitHub required status checks, Kubernetes admission webhooks, Shopify's pre-purchase functions, database triggers with BEFORE INSERT semantics. It is a reasonable thing to expect.
Why it doesn't exist in Webflow
There is no pre-publish event. Every webhook trigger type Webflow offers is a notification about something that has already happened. The complete list:
| Event | Fires |
|---|---|
form_submission | After a form is submitted |
site_publish | After the site has been published |
page_created | After a page is created |
page_metadata_updated | After page metadata is updated and published |
page_deleted | After a page is deleted |
ecomm_new_order | After an order is placed |
ecomm_order_changed | After an order changes |
ecomm_inventory_changed | After inventory changes |
collection_item_created | After a CMS item is created |
collection_item_changed | After a CMS item is updated |
collection_item_deleted | After a CMS item is deleted |
collection_item_published | After a CMS item is published |
collection_item_unpublished | After a CMS item is unpublished |
comment_created | After a comment or reply is created |
There is no site_pre_publish, no before_publish, no publish_requested. site_publish is a past-tense notification -- by the time it reaches your endpoint, the content is already on the CDN.
The delivery model is fire-and-forget, not request-response. Even if the timing were right, the semantics wouldn't be. Webflow expects a 200 from your endpoint. Anything else is treated as a delivery failure, not as a business decision:
- Non-
200responses are retried up to 3 times, at 10-minute intervals. - Redirects, SSL negotiation failures, and timeouts are also treated as failures.
- After repeated failures, Webflow deactivates the webhook and emails you about it.
So returning 403 Rejected doesn't block a publish. It queues 30 minutes of pointless retries and then silently turns off your listener. The failure mode of the "obvious" approach is worse than doing nothing, because you lose observability as well.
Designer Extensions can't intercept the button either. Apps built on the Designer API run inside the canvas and operate on elements, styles, and pages. They have no hook into the publish action and cannot present a blocking modal in front of it.
Conclusion: There is no veto point in Webflow's publish pipeline. Any design that depends on Webflow asking for permission before publishing cannot be built today. Note this as a limitation and design around it -- do not build a webhook listener that returns error codes hoping it will act as a gate.
The principle that does work: invert control
You cannot block an action you don't own. So stop trying to intercept the publish, and instead become the only party capable of performing it.
Every workable gating architecture in Webflow is a variation on three moves:
- Remove the ability to publish from the humans who would otherwise do it.
- Interpose a service that holds the credentials, evaluates your criteria, and owns the decision.
- Provide a trigger surface -- a button, a command, a pipeline -- that lets people request a publish instead of performing one.
The publish stops being a UI action and becomes an API call your infrastructure makes on someone's behalf. Everything below is the practical shape of that.
Part 1 -- Site publishing
Full-site publish is the highest-risk path: it pushes everything staged across the entire site. It's also the one with no native content-level gate, so this is where the server-owned model matters most.
Step 1: Lock down publish permissions
The gate is worthless if anyone can walk around it. Webflow's site-level roles are the enforcement mechanism.
- Site members can be assigned Site admin, Can design & publish, Can design, or Can design (limited). Move everyone who shouldn't be pushing to production onto a role without publish.
- Content editors can be set to Can edit or Can edit & publish. Restricting publish here prevents a full-site publish, which is the main protection against pushing someone else's in-progress design work live.
- Publish permission is controllable per-site, so you can be strict on production properties and relaxed on sandboxes.
Known leak: editors restricted from publishing can still publish individual CMS items that don't affect in-progress designs. This is by design -- it lets marketing ship content while design work is pending. It means role restrictions alone are not a complete CMS gate. See Part 3.
Step 2: Build the gate service
The gate service is a small, boring piece of infrastructure. It needs four things.
Credential custody. It holds the Webflow token (site token or an OAuth Data Client App token with sites:write). No human has a token that can publish production. This is the actual gate -- everything else is workflow around it.
An intake. Something that receives "I would like to publish" and records who asked, when, and why.
A criteria evaluator. A set of independent checks, each returning pass/fail with a human-readable reason. Common ones:
| Category | Example checks |
|---|---|
| Content quality | Broken internal links, missing alt text, placeholder copy (Lorem, TODO, FIXME), empty required CMS fields |
| SEO | Missing or duplicate titles/meta descriptions, missing OG images, unintended noindex, sitemap validity |
| Structural | Expected pages present, canonical tags correct, redirect map intact |
| Process | Ticket in the right state, approval recorded, change log entry exists |
| Temporal | Content freeze windows, no Friday-afternoon deploys, embargo dates not yet reached |
| External state | Feature flag enabled, backing API deployed, legal sign-off recorded |
A publish executor. On unanimous pass, it calls the Webflow Data API publish endpoint. On any failure, it doesn't -- and it reports back which check failed and why.
// Sketch -- not production code
app.post('/publish-requests', requireAuth, async (req, res) => {
const request = await recordRequest({
siteId: req.body.siteId,
target: req.body.target, // 'staging' | 'production'
requestedBy: req.user.id,
reason: req.body.reason,
});
const results = await Promise.all(
CHECKS.map(check => check.run(request).catch(err => ({
name: check.name, passed: false, reason: `Check errored: ${err.message}`
})))
);
const failures = results.filter(r => !r.passed);
if (failures.length) {
await recordDecision(request.id, 'rejected', failures);
await notify(request.requestedBy, failures);
return res.status(200).json({ status: 'rejected', failures });
}
const publish = await webflow.sites.publish(request.siteId, {
// target domains resolved from request.target
});
await recordDecision(request.id, 'published', results, publish);
return res.status(200).json({ status: 'published' });
});Note the 200 on rejection -- the gate service is your API, not Webflow's, so use whatever status codes you like. The point is that the rejection is a decision that is reported, not an error that is retried.
Step 3: Give people a trigger surface
If requesting a publish is harder than publishing was, the process will be routed around. Options, roughly in order of adoption success:
- Browser plug-in. Make it clear if the site is encumbered by active editing ( red yellow green indicator ), and request a publish. This can also be used to notify everyone else that they should wrap up and release so your changes can go through.
- Slack slash command (
/publish-site production) with the result posted back in-channel. Lowest friction, and the audit trail is a side effect. - Internal dashboard with a request button, live check results, and history. Best when non-technical stakeholders need visibility into why something was blocked.
- CI/CD pipeline triggered on merge to a release branch. Natural when Webflow content is coupled to code deploys.
- Scheduled reconciliation -- the gate service runs checks on a cadence and publishes automatically when everything is green and there are pending changes. This can be scheduled like normal dev workflows, e.g. every Friday night. That also allows a kick-off of validation processes that audit the site after changes occur.
Technique: gate the domain, not the action
A useful softening. Webflow publishes to staging (*.webflow.io) and production domains independently. Rather than blocking all publishing:
- Let the team publish freely to staging. Fast iteration, no ceremony, no gate.
- Route production exclusively through the gate service.
This preserves the design workflow almost entirely while putting the control where the risk is. It also makes the criteria evaluator's job easier -- it can inspect the staged site directly, which is a much better source of truth than the Designer's unpublished state.
Technique: detection and reconciliation
site_publish can't prevent anything, but it's still worth wiring up as a tripwire:
- Subscribe to
site_publish. - On receipt, compare the publish against your record of authorised requests.
- If it doesn't correlate to an approved request, you have an out-of-band publish -- someone with a lingering permission, a legacy token, or a Webflow-side action you didn't anticipate.
- Alert loudly. Run your criteria post-hoc and report violations.
This is monitoring, not enforcement. Treat "an unrecognised publish occurred" as a permissions bug to fix, not as an event to remediate automatically.
Do not build detect-and-revert. The pattern of listening onsite_publish, running checks, and rolling back on failure is tempting and bad. Bad content is live for the whole detection-and-rollback window, backup restoration isn't cleanly scriptable, and a revert can silently destroy legitimate concurrent work. Usesite_publishto know, not to undo.
Enterprise: page branching and design approvals
On Enterprise plans, Webflow provides a native human-approval gate. This is the closest thing to first-party publish gating, and it applies to the site publishing story.
How it works. A designer branches a page, works on it independently while the original stays live, and then requests a review. Designated approvers approve or request changes. Only an approved branch can be merged into the main site.
Role behaviour. A team member's site role determines whether they need approval or can grant it. Site managers, designers, marketers, and content editors can request a review; reviewers, content editors, and marketers cannot merge branches. If your role requires design approval, you can only edit in branches and must be approved before merging.
Supporting mechanics. Branches support real-time multi-user collaboration and are Localize-compatible. You can pull updates from main into a branch mid-flight, and stage a branch to a preview URL. Branched pages don't count against the static page limit.
Critical caveat.Page branching does not block full-site publishing. Teammates can publish the main site at any time without publishing branches. The approval gates the merge, not the publish. If unapproved changes reach main by any other route, a full-site publish will ship them.
Other limitations worth knowing:
- Merging overwrites the original page's content. Changes made to the original page while it was branched are lost -- though components, classes, and interactions survive.
- Merging or deleting a staged branch unpublishes and deletes its staging URL.
- Branches don't carry over when a site is transferred or duplicated.
- You must publish the site before you can stage a branch.
Recommended combination. Enterprise approvals and the server-owned gate are complementary, not alternatives:
- Approvals handle the subjective criteria -- "does this look right," "is this on-brand," "did legal sign off."
- The gate service handles the objective criteria -- broken links, missing metadata, freeze windows, external system state.
- Permission lockdown closes the full-site publish escape hatch that branching leaves open.
If your criteria are genuinely "a human should look at this," use the native feature. Don't build a bespoke approval UI to duplicate it. If your criteria are mechanically checkable, a machine should check them, and approvals aren't the right tool.
Part 2 -- Page-level publishing as mitigation
Single-page publish is a blast-radius control. It doesn't gate anything by itself, but it changes the unit of risk from "the entire site" to "one page," which makes every other control more tractable.
What it gives you
- Granular API-driven publishing. Single-page publish can be triggered through the Webflow v2 API, and is explicitly positioned for automated publishing workflows and CI/CD integrations. Your gate service can therefore approve and publish at page granularity rather than all-or-nothing.
- Per-page criteria. Checks scoped to one page are faster, cheaper, and produce far more actionable failure messages than a whole-site sweep.
- Permission alignment. Publishing a single page requires a role with the Can publish permission. If a role has page-specific access with publishing restrictions, that user can only publish the pages they're permitted to edit. This lets you scope publish rights to page ownership -- a campaign team that can only ever publish campaign pages.
The gotcha that undermines naive use
Publishing to production includes all changes currently staged -- not just your page. Webflow's own guidance is to publish to staging, review, and then choose between:
- Publish to all domains -- updates just your page on production.
- Publish to production -- makes production fully match staging.
If your gate service validates page A and then calls a publish that syncs everything staged, it has just shipped unvalidated pages B through Z under the authority of a check that never looked at them. If you are gating at page level, you must be explicit about the target and understand exactly which of these two behaviours your API call produces. Validate this against a staging site before trusting it in production.
Other constraints
- No scheduling. You can't schedule a single-page publish for a future time. Scheduling requires a full-site publish workflow -- or your own scheduler calling the API, which is what the gate service is for anyway.
- One publish at a time. Only one publish can run per site, full-site or single-page. If a publish is in progress, Webflow shows who's publishing and the button is unavailable. Your gate service needs to handle contention: queue requests, retry on conflict, and never assume a publish call will succeed immediately.
- Main branch only. You can't single-page publish from a branch environment. Merge to main first.
How it fits with branching
Webflow positions these as complementary workflows:
- Single-page publish for fast, isolated, low-risk changes scoped to one page -- a campaign launch, a copy fix, a page-level test.
- Page branching for changes that need review, parallel work, or comparison before going live.
For gating purposes: use page-level publishing to narrow what your gate has to reason about, and branching to hold changes that need human judgement.
Part 3 -- CMS item publishing
This is the good news section. CMS items are the one place Webflow gives you a genuine pre-live gate, because item creation and item publication are separate operations.
The draft state is your gate
An item can exist in the CMS in an unpublished draft state. It's fully addressable via the API -- you can read it, validate it, and modify it -- while being invisible on the live site. That gap between "exists" and "is live" is exactly the interception point that site publishing lacks.
The pattern
Author creates/edits item (draft)
│
▼
collection_item_created / collection_item_changed ──► Your validator
│ │
│ ┌────────────┴────────────┐
│ │ │
▼ PASS FAIL
(item stays draft) │ │
▼ ▼
Publish item via API Write failure reason
→ now live to a status field
+ notify author
→ item stays draftStep 1 -- Author in draft. Establish the convention that items are created as drafts. Where content originates from an external system, have your integration create items in draft state by default.
Step 2 -- Subscribe to collection_item_created and collection_item_changed. These are still post-event webhooks, but the event they report is creation, not publication. The item isn't live yet, so post-event is fine. This is the structural difference that makes CMS gating work.
Note: a single webhook registration takes one triggerType. Register these separately.
Step 3 -- Validate. Fetch the full item, run your rules: required fields populated, reference integrity, image assets present and correctly sized, slug format and uniqueness, word count thresholds, banned terms, embargo date reached, an approver recorded in a dedicated field.
Step 4 -- Act.
- Pass: call the API to publish the item. It goes live, having been validated.
- Fail: leave it as a draft and write the reason somewhere the author will see it. A
Validation StatusandValidation Notesfield pair on the collection works well -- the feedback appears in the Editor next to the content that caused it.
Step 5 -- Guard the leak. Editors restricted from full-site publishing can still publish individual CMS items. That's a real hole in the fence. Mitigate by:
- Assigning Reviewer or Can edit roles to authors who must not publish directly.
- Subscribing to
collection_item_publishedas a tripwire -- on receipt, re-run validation, and if it fails, set the item back to draft and notify. Unlike site-level revert, this is safe: it's scoped to a single item, it's a supported API operation, and it destroys nothing.
Why this one actually works
CMS gating succeeds where site gating fails for a structural reason worth internalising: Webflow separates the CMS item lifecycle into distinct create and publish operations, and exposes both to the API. Site publishing is a single atomic operation with no addressable intermediate state.
Wherever a platform separates "written" from "live," you can gate. Wherever it doesn't, you have to own the trigger. That's the whole article in two sentences.
Reference: webhook constraints
Useful when sizing an integration.
| Constraint | Value |
|---|---|
Max webhooks per triggerType, per site | 75 |
| Retry attempts after a failed delivery | 3 |
| Interval between retries | 10 minutes |
| Expected success response | HTTP 200 |
| Treated as failure | Non-200, redirects, SSL errors, timeouts |
| Consequence of repeated failure | Webhook deactivated, email notification sent |
| Signature headers | x-webflow-timestamp, x-webflow-signature |
| Signature algorithm | SHA-256 HMAC over timestamp + ":" + body |
| Signing key | Per-webhook secret (site token, post-April 2025) or OAuth app client secret |
| Replay window | Reject requests older than 5 minutes |
Always validate signatures. A gate service that publishes in response to webhooks is a remote-trigger endpoint; an unsigned one is an open door. Use the SDK's verifySignature method rather than hand-rolling HMAC comparison -- Webflow maintains it across signature scheme changes, so you only need to bump the package version.
Note that webhooks created through the Webflow dashboard do not include the headers needed for signature validation. If signature verification matters -- and for a publish gate it does -- create your webhooks via the API, not the UI.
Choosing an approach
| Your criteria are... | Use |
|---|---|
| Subjective, human judgement, design review | Enterprise page branching + design approvals |
| Mechanically checkable, site-wide | Permission lockdown + server-owned gate service |
| Mechanically checkable, per-page | Same, scoped via single-page publish API |
| Content-level, per record | CMS draft state + validate-then-publish |
| Time-based (freeze windows, embargoes) | Gate service with a scheduler |
| Dependent on external system state | Gate service polling or subscribing to that system |
| "Just tell me when something goes live" | site_publish / collection_item_published webhooks |
Summary
- Webflow has no pre-publish hook. All webhook events are post-event notifications, and non-200 responses trigger retries and eventual webhook deactivation rather than rejecting anything. Pre-approval via webhook is not achievable.
- Gate by ownership, not interception. Strip publish permissions, put the token in a service, expose a request surface.
- Gate the domain. Leave staging open; route production through the gate.
- Use Enterprise approvals for human judgement -- but remember they gate the merge, not the publish, and full-site publish bypasses them.
- Use page-level publishing to shrink blast radius -- while being precise about whether your publish call syncs the whole staging state.
- CMS items are genuinely gateable via the draft/publish split. This is the strongest control Webflow offers.
- Use
site_publishas a tripwire, never as a rollback trigger.
Verify before you build
API endpoint shapes, scopes, and plan-tier availability change. Confirm the current specifics -- publish endpoints, item draft semantics, and role capabilities on your plan -- against the live Webflow developer documentation and Help Center before committing to an implementation.
