Skip to content
Dashboard

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.published event

Step 1 — Generate a Webflow API Token

1

Open your Webflow dashboard and click the site you want to publish to.

2

Go to Site Settings → Apps & Integrations → API access.

3

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 fieldWebflow field slug (example)Webflow field type
titlenamePlain text (built-in)
slugslugSlug (built-in)
content_htmlpost-bodyRich Text
excerptpost-summaryPlain text
featured_image_urlthumbnail-imageImage (URL)
published_atpublished-onDate

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.