Overview
Webflow is a visual web design tool with its own CMS, hosting and forms. Designers build pages in the Designer; developers and automations use the Data API v2 (host api.webflow.com, paths start with /v2) to read and write CMS content, publish, read form submissions and register webhooks. Webflow also runs a hosted MCP server so an AI agent can work on a site, and a code components workflow (React components imported into the Designer).
The Data API v1 is a separate, older version; everything here is v2. Webflow's docs expose clean Markdown by appending .md to a page URL and a section index at https://developers.webflow.com/data/llms.txt.
Instructions
Step 1: Get a token
Every call sends Authorization: Bearer <token>.
- Site token: made in Site settings, Apps & integrations, API access. Scoped to one site and the scopes you tick (for CMS work
cms:readandcms:write, plussites:writeto publish the site). Best for internal scripts. - Workspace token: read access across a Workspace, for audits.
- OAuth app token: for public integrations and Marketplace apps.
Keep tokens in environment variables (WEBFLOW_API_TOKEN, WEBFLOW_SITE_ID), never in a Designer custom-code field or client-side JavaScript. Custom-code endpoints need an OAuth app, not a site token.
curl -s https://api.webflow.com/v2/sites -H "Authorization: Bearer $WEBFLOW_API_TOKEN"
curl -s https://api.webflow.com/v2/sites/$WEBFLOW_SITE_ID/collections -H "Authorization: Bearer $WEBFLOW_API_TOKEN"
GET /collections/{collection_id} returns the field list. Field slug values (not display names) are the keys of fieldData.
Step 2: Create, update and publish CMS items
Items live in two states. A staged item is a draft or pending change; a live item is what visitors see. Writes go to the staged copy unless you use a /live endpoint.
// src/webflow.ts
const API = "https://api.webflow.com/v2";
const headers = {
Authorization: `Bearer ${process.env.WEBFLOW_API_TOKEN}`,
"Content-Type": "application/json",
};
async function wf<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API}${path}`, { ...init, headers });
if (res.status === 429) {
const wait = Number(res.headers.get("Retry-After") ?? 60);
await new Promise((r) => setTimeout(r, wait * 1000));
return wf<T>(path, init);
}
if (!res.ok) throw new Error(`Webflow ${res.status}: ${await res.text()}`);
return res.json() as Promise<T>;
}
// Create up to 100 items per request (POST /items/insert is the endpoint for new work)
export async function createPosts(collectionId: string, posts: { title: string; slug: string; html: string; imageUrl: string }[]) {
return wf<{ items: { id: string }[] }>(`/collections/${collectionId}/items/insert`, {
method: "POST",
body: JSON.stringify({
items: posts.map((p) => ({
isDraft: false, // queue for the next publish; default is true
fieldData: {
name: p.title,
slug: p.slug,
"post-body": p.html, // rich text is an HTML string
"thumbnail-image": { url: p.imageUrl }, // public URL, max 4 MB
},
})),
}),
});
}
// Update one item (PATCH, not PUT)
export const updateTitle = (collectionId: string, itemId: string, name: string) =>
wf(`/collections/${collectionId}/items/${itemId}`, { method: "PATCH", body: JSON.stringify({ fieldData: { name } }) });
// Publish specific items without publishing the whole site
export const publishItems = (collectionId: string, itemIds: string[]) =>
wf<{ publishedItemIds?: string[]; errors?: string[] }>(`/collections/${collectionId}/items/publish`, {
method: "POST",
body: JSON.stringify({ itemIds }),
});
Facts that bite:
- The create and publish endpoints answer
202; the work finishes asynchronously. - New items are drafts by default.
isDraft: falsequeues them for the next site publish;publishItemsmakes them live now. isDraft: trueon an already-published item keeps it live and holds the edit as a draft. To take items off the live site callDELETE /collections/{id}/items/livewith{"items": [{"id": "..."}]}(up to 100, answers204, and setsisDraftback totrue); settingisArchived: trueremoves it only at the next site-wide publish.- Update a live item and publish in one call with
PATCH /collections/{id}/items/{item_id}/live. - Older
POST /itemsandPOST /items/bulkstill work but are no longer listed; they take one locale per item. Create Items (/items/insert) takescmsLocaleIds(plural) orallCmsLocales: trueand rejects unknown properties with400. - List endpoints return at most 100 items; page with
offsetandlimit. Filter withfilter[<fieldSlug>][<operator>]=valueand sort withsort[<fieldSlug>]=asc. - Rich text API writes drop code blocks (an empty string results). Multi-image, reference and option fields have their own value shapes; see the Field Types reference.
Step 3: Publish the site
curl -s -X POST "https://api.webflow.com/v2/sites/$WEBFLOW_SITE_ID/publish" \
-H "Authorization: Bearer $WEBFLOW_API_TOKEN" -H "Content-Type: application/json" \
-d '{"publishToWebflowSubdomain": true}'
Include publishToWebflowSubdomain and/or customDomains (domain IDs from GET /sites/{id}/custom_domains). Add pageId to publish one page. A site publish is limited to one successful call per minute.
Step 4: Serve live content fast
For read-only public content, call the Content Delivery API: same paths, host api-cdn.webflow.com, only the /items/live reads. Cached responses carry cf-cache-status: HIT and are effectively not rate limited, while uncached calls count against your plan limit.
curl -G "https://api-cdn.webflow.com/v2/collections/$COLLECTION_ID/items/live" \
-H "Authorization: Bearer $WEBFLOW_CDN_TOKEN" -d limit=100 -d offset=0
Use a separate token with only cms:read for each delivery channel.
Step 5: Webhooks with signature checks
Create webhooks through the API (POST /sites/{site_id}/webhooks with triggerType, url; needs sites:write plus the event's own read scope, such as forms:read), because webhooks made in the dashboard do not send signature headers. Only form_submission supports a filter ({"name": "Contact Us"}). Event types include form_submission, site_publish, collection_item_created, collection_item_changed, collection_item_deleted, collection_item_published, collection_item_unpublished, ecomm_new_order. Failed deliveries are retried up to 3 times, 10 minutes apart.
import { WebflowClient } from "webflow-api"; // npm install webflow-api
import express from "express";
const webflow = new WebflowClient({ accessToken: process.env.WEBFLOW_API_TOKEN! });
const app = express();
app.use(express.json());
app.post("/webhooks/webflow", async (req, res) => {
const age = Date.now() - Number(req.header("x-webflow-timestamp"));
const valid = await webflow.webhooks.verifySignature({ // returns a Promise: await it
headers: req.headers as Record<string, string>,
body: JSON.stringify(req.body),
secret: process.env.WEBFLOW_WEBHOOK_SECRET!,
});
if (!valid || !(age >= 0 && age < 5 * 60 * 1000)) return res.sendStatus(401);
// handle req.body.triggerType / req.body.payload
res.sendStatus(200);
});
verifySignature checks the HMAC-SHA256 of timestamp:body only; it does not reject an old timestamp (verified with webflow-api 3.3.4), so also enforce the 5-minute window yourself, as above. The signing key is the webhook's own secret (webhooks made with a site token after 2025-04-14) or the OAuth app's client secret.
Step 6: Let an agent work on the site (MCP)
claude mcp add --transport http webflow https://mcp.webflow.com/mcp
In Claude Code run /mcp, pick webflow and finish the OAuth login in the browser, choosing which sites to allow. The server can read and edit CMS collections and items, pages, SEO metadata, forms, assets and components; some Designer actions need the MCP Bridge App open in the Designer. It works within your Webflow roles.
Step 7: Code components (React in the Designer)
npm i --save-dev @webflow/webflow-cli @webflow/data-types @webflow/react # Node 22.13+
Add webflow.json with {"library": {"name": "Acme UI", "components": ["./src/**/*.webflow.@(js|jsx|mjs|ts|tsx)"]}}, then write one Badge.webflow.tsx per component that calls declareComponent(Badge, { name, props }). Check the code components quickstart for the plan it requires before promising this to a client.
Examples
Example 1: Sync blog posts from a headless CMS into Webflow
User prompt: "Every night copy new posts from our Contentful space into the Webflow Blog collection and publish them."
The agent reads GET /collections/{id} to find the real field slugs (post-body, thumbnail-image), maps each Contentful entry to fieldData, batches up to 100 per POST /collections/{id}/items/insert call with isDraft: true, collects the returned items[].id, then calls POST /collections/{id}/items/publish with those ids. It handles 429 by honoring Retry-After. Result: {"publishedItemIds":["66ffa1d3c0..."],"errors":[]}. Re-runs look items up by slug (GET .../items?slug=my-post) and PATCH instead of creating duplicates.
Example 2: Slack alert for every form submission
User prompt: "Post a Slack message when someone submits the Contact Us form on our Webflow site."
The agent registers a webhook once:
curl -s -X POST "https://api.webflow.com/v2/sites/$WEBFLOW_SITE_ID/webhooks" \
-H "Authorization: Bearer $WEBFLOW_API_TOKEN" -H "Content-Type: application/json" \
-d '{"triggerType":"form_submission","url":"https://hooks.acme-web.dev/webflow","filter":{"name":"Contact Us"}}'
It stores the webhook's signing key (or the OAuth app's client secret) as WEBFLOW_WEBHOOK_SECRET, deploys the handler from Step 5, and forwards payload.data (name, email, message) to Slack after the signature and timestamp checks pass. The token needs forms:read.
Guidelines
- API limits: 60 requests per minute on Starter and Basic site plans, 120 on plans with CMS, per API key;
429carriesRetry-After. Batch writes (up to 100 items) and use webhooks instead of polling. - Use slugs from the collection definition, not field labels. A wrong key is rejected with a validation error.
- Publishing is separate from writing. If content "isn't showing up", check
isDraft,isArchivedandlastPublishedon the item, and whether the site was published after a staged change. - Site-wide publish publishes all staged changes, including other people's drafts. Prefer
items/publishfor targeted releases. - Images must be on a public URL and at most 4 MB when uploaded via the API.
- Site plans and prices change: Starter is free with limited CMS and 2 static pages; check the Webflow pricing page before promising CMS item counts, bandwidth or form limits.
- Do not use Webflow for server-side logic or heavy transactional apps; use Webflow Cloud or your own backend and keep Webflow for marketing pages, CMS content and forms.
- In the Designer: use classes and combo classes (for example
.cardplus.is-featured) instead of per-element styling, and reusable components for navbar, footer and CTA blocks.