Webflow Integration
Push Brainpercent-generated articles directly into a Webflow CMS Collection — title, rich text body, slug, cover image, and publish date — using Webflow's REST API. Works with both a manual import approach and a live webhook-driven pipeline.
What You Need
- A Webflow account with a site on the CMS plan or higher
- A CMS Collection on your Webflow site for blog posts (must already exist)
- A Webflow API token (Site Settings → Apps & Integrations → API access)
- A Brainpercent webhook URL for the
article.publishedevent
Step 1 — Generate a Webflow API Token
Open your Webflow dashboard and click the site you want to publish to.
Go to Site Settings → Apps & Integrations → API access.
Click Generate API Token. Copy the token — it is only shown once.
Note: Webflow API tokens are site-scoped. The token gives full CMS read/write access to the selected site. Store it securely and never commit it to source control.
Step 2 — Find Your CMS Collection ID
The Webflow API requires the Collection ID, not the collection name. Retrieve it via the API:
# Replace YOUR_API_TOKEN and YOUR_SITE_ID
curl -s "https://api.webflow.com/v2/sites/YOUR_SITE_ID/collections" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "accept: application/json" | \
python3 -c "import sys,json; [print(c['id'], c['displayName']) for c in json.load(sys.stdin)['collections']]"To find your Site ID, run:
curl -s "https://api.webflow.com/v2/sites" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "accept: application/json" | \
python3 -c "import sys,json; [print(s['id'], s['displayName']) for s in json.load(sys.stdin)['sites']]"Note the Collection ID for your blog posts collection (e.g. 64a3c8e1f2b3c4d5e6f7a8b9). You will need it in every subsequent API call.
Step 3 — Map Brainpercent Fields to Webflow CMS Fields
Webflow CMS Collections have a custom schema that you define in the Webflow Designer. The field slugs (API names) are lowercase with hyphens and set when you create the field. Common mapping for a blog collection:
| Brainpercent field | Webflow field slug (example) | Webflow field type |
|---|---|---|
title | name | Plain text (built-in) |
slug | slug | Slug (built-in) |
content_html | post-body | Rich Text |
excerpt | post-summary | Plain text |
featured_image_url | thumbnail-image | Image (URL) |
published_at | published-on | Date |
Field slugs are set in the Webflow Designer when you create a field and cannot easily be changed later without breaking existing data. Use lowercase hyphenated slugs from the start (e.g. post-body, not postBody).
Step 4 — Webhook Handler
The Webflow API v2 creates CMS items at POST /v2/collections/{collectionId}/items. Use isArchived: false and isDraft: false to publish immediately, or set isDraft: true to create a draft for review.
import crypto from 'crypto';
import express from 'express';
const app = express();
const WF_TOKEN = process.env.WEBFLOW_API_TOKEN;
const WF_COLLECTION = process.env.WEBFLOW_COLLECTION_ID;
const BP_SECRET = process.env.BP_WEBHOOK_SECRET;
const PUBLISH_AS_LIVE = true; // set false to create drafts
function verifySignature(rawBody, signature) {
const expected = crypto
.createHmac('sha256', BP_SECRET)
.update(rawBody)
.digest('hex');
return `sha256=${expected}` === signature;
}
app.post('/webhooks/brainpercent', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verifySignature(req.body, req.headers['x-brainpercent-signature'])) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
if (event.event !== 'article.published') {
return res.json({ skipped: true });
}
const article = event.data;
// Build Webflow CMS item payload
// Adjust field slugs to match YOUR collection's field names
const itemPayload = {
isArchived: false,
isDraft: !PUBLISH_AS_LIVE,
fieldData: {
name: article.title,
slug: article.slug,
'post-body': article.content_html,
'post-summary': article.excerpt ?? '',
'published-on': article.published_at ?? new Date().toISOString(),
...(article.featured_image_url && {
'thumbnail-image': { url: article.featured_image_url, alt: article.title },
}),
},
};
const wfRes = await fetch(
`https://api.webflow.com/v2/collections/${WF_COLLECTION}/items`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${WF_TOKEN}`,
'Content-Type': 'application/json',
'accept': 'application/json',
},
body: JSON.stringify(itemPayload),
}
);
if (!wfRes.ok) {
const err = await wfRes.text();
return res.status(502).json({ error: err });
}
const item = await wfRes.json();
res.json({ webflow_item_id: item.id });
});
app.listen(3000);Alternative — Manual Import via CSV
Webflow supports CSV import into CMS Collections (Designer → CMS → Collection → Import). Export a batch of Brainpercent articles as CSV with columns matching your Webflow field slugs. This is useful for a one-time bulk migration.
Consult Webflow's official docs at university.webflow.com for CSV format requirements, especially the Rich Text column format.
Common Gotchas
- Rich Text field rejects raw HTML? Webflow's Rich Text field via API accepts a limited subset of HTML. Some tags (e.g.
div,script) are stripped. Refer to developers.webflow.com for the allowed tag list. - Slug conflicts? Webflow returns a 400 if a slug already exists in the collection. Append a timestamp suffix to deduplicate:
`${article.slug}-${Date.now()}`. - Image field not accepting URL? Webflow's Image field expects an object
{ url: '...', alt: '...' }, not a plain string. The alt text is required. - Items created but not visible on site? After creating an item via API you still need to publish the site in Webflow. Use the Publish API (
POST /v2/sites/{siteId}/publish) or trigger a manual publish in the Designer. - Rate limit: 60 requests/minute per API token on most Webflow plans. Batch large imports with a delay between requests.
For the full Brainpercent webhook payload schema, see Webhooks & Async guide.