Enterprise APIs and Governance

This content was generated with the assistance of AI and should be verified against the official Webflow documentation before being relied on in production.

This sub-section’s Webflow bibliography lists the reference material consulted while preparing these pages.

This page describes the Webflow Enterprise API surface and the governance features around it as they were on 2026-10-28, the date on which its facts were checked against the Data API reference on developers.webflow.com (authentication, scopes, rate limits, workspace tokens and every endpoint in the Enterprise reference section). The pinned developer packages are webflow-api 3.3.4 for JavaScript and TypeScript and webflow 2.0.0 for Python. Every endpoint in the sections below requires an Enterprise workspace; plans are named by tier only, never by price, and a plan change by Webflow can move an endpoint between tiers, so the reference page of an endpoint is the authority for its current requirement. The examples on this page are written to be run by an organisation that has an Enterprise workspace and are shown with placeholder identifiers and tokens only; the Developer workspace is the free place to rehearse the plan-neutral parts of the Data API.

The running example is Fernbrook Gardens, a fictitious botanical garden. For most of this sub-section it is a small team with one site. Imagine now that it has grown into a federation: the garden itself, a sister arboretum and a seed-exchange charity, each with its own Webflow site, a central web team, volunteers who edit content, and a trustee board that asks, once a year, who had access to what. That is the situation in which the Enterprise APIs matter.

What "Enterprise" adds for developers

Most of the Data API is plan-neutral: Collections, items, pages, assets, forms, webhooks and the site publish endpoint work for any site that a token can reach (see Data API: Authentication and Scopes and Data API: SDKs, Limits and Errors). Enterprise adds four kinds of capability, each with its own section of the reference:

  • Workspace-level administration: list the Workspaces a token can see, create, rename and delete Sites, read a site’s hosting plan.

  • People and access: list, invite, re-role and remove Workspace members, and grant or revoke access to a single Site, including custom Site roles.

  • Site configuration: manage 301 redirects, the robots.txt rules and the files served under /.well-known/.

  • Observability and audit: read the Workspace audit log (who logged in, who was added or removed, which role changed) and the Site Activity log (what changed on a Site, by whom or by which agent).

A fifth, smaller piece is the workspace token, a token type that exists only for Enterprise Workspaces and that is the normal credential for the audit log.

flowchart TB ENT["Enterprise workspace"] --> WT["Workspace API token"] ENT --> APP["Data Client app or site token"] WT --> AUD["Workspace audit logs"] WT --> WLIST["List workspaces"] APP --> SITES["Create, update, delete sites"] APP --> CFG["Site configuration: redirects, robots.txt, well-known files"] APP --> ACC["Site access and workspace members"] APP --> ACT["Site activity logs"] AUD --> SIEM["SIEM or log store"] ACT --> SIEM

Credentials and scopes

The Enterprise endpoints use the same bearer-token scheme as the rest of the Data API, with a few extra scopes and one extra token type. The authoritative lists are the Scopes reference and the Workspace Token page.

Workspace tokens

A workspace token is an API token that grants access to Workspace-level resources, such as the audit log. It is meant for Workspace administrators. Site data is reached with a site token instead; a workspace token has no site scope and therefore cannot call a Site endpoint such as Get Site Information.

The rules documented for workspace tokens are:

  • They exist only for Enterprise Workspaces, and only Workspace administrators can create them.

  • A Workspace can have up to five of them.

  • A token expires after 365 consecutive days without use; any API call resets the countdown.

  • A token cannot be regenerated. If its value is lost, create a new token and update every integration that used the old one.

  • The only workspace-token scope in the documentation is workspace_activity:read, which grants the audit log endpoint. The documentation also lists workspace:read (for List Workspaces) among the scopes supported by workspace tokens.

Creating a token is a Dashboard task. In the Workspace, open Apps & integrations, choose Manage, scroll to Workspace API access, and use Generate API token. Name it, set the permission of each scope (for example no access or read-only), generate it and copy the value once into a secrets manager.

Fernbrook’s security practice follows the guidance of the same page: one token per use case (the SIEM forwarder has its own token, separate from the quarterly access-review script), descriptive names, the minimum scope, periodic rotation, and immediate revocation when an administrator leaves or changes role.

The scopes that matter

The Scopes reference lists the following scopes for the endpoints on this page. The "Supported by" column is from the reference; a scope that a token type does not support cannot be granted to it.

Scope Level Covers and supported by

workspace:read

Workspace

List Workspaces. Workspace API tokens and Data Client apps registered with workspace-level access. New site tokens cannot have this scope.

workspace_activity:read

Workspace

The Workspace audit log. Workspace API tokens and Data Client apps registered with workspace-level access.

workspace:write

Site

Create Site. Data Client apps and site tokens.

workspace_members:read, workspace_members:write

Workspace

List, invite, re-role and remove Workspace members (named in the endpoint pages).

site_access:read, site_access:write

Site

List and change the members of one Site (named in the endpoint pages).

site_activity:read

Site

The Site Activity log. Data Client apps and site tokens.

site_config:read, site_config:write

Site

Site Configuration endpoints; site_config:write is the scope of the robots.txt and well-known file endpoints.

sites:read, sites:write

Site

Sites endpoints. sites:read also lists the 301 redirects and reads the site plan; sites:write creates, updates and deletes redirects and updates or deletes a Site.

branches:read, branches:create, branches:update, branches:delete, branches:merge, branches:publish

Site

The page-branching endpoints.

site_plans:read, site_plans:write

Site

Site plan endpoints.

There is no scope named "enterprise"; the plan check happens on the workspace, and a call from a token whose workspace is not Enterprise fails with an error rather than with a missing scope. A call that lacks a scope fails with a 403 whose code is missing_scopes, so the two causes are distinguishable in logs.

Rate limits and retries

Rate limits are per API key and are published per plan tier: 60 requests per minute for Starter and Basic, 120 for CMS, Ecommerce and Business, and custom limits for Enterprise. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a response with status 429 carries Retry-After, typically 60 seconds. The official SDKs retry 429s automatically with exponential backoff; code that calls the REST endpoints directly must honour Retry-After. Because Enterprise limits are negotiated, an audit-log forwarder should read X-RateLimit-Remaining rather than hard-code a number. The Site publish endpoint has its own limit of one successful publish per minute.

Listing Workspaces and finding the identifier

Most Workspace endpoints take a workspace_id in the path. GET /v2/workspaces lists the active Workspaces a token can reach, with each id, name and slug; archived, deleted and draft Workspaces are excluded and the list is not paginated. It needs workspace:read, and it works with Workspace API tokens and with Data Client apps. The audit log endpoint accepts either the ID or the slug.

curl --request GET \
  --url https://api.webflow.com/v2/workspaces \
  --header 'accept: application/json' \
  --header "authorization: Bearer $WEBFLOW_WORKSPACE_TOKEN"

Site lifecycle: create, update, delete and plan

Four endpoints manage Sites from outside the Dashboard. They let a central team provision a site for each new programme from a template, rename and file it, and retire it.

Operation Request Scope Notes

Create Site

POST /v2/workspaces/<workspace_id>/sites

workspace:write

Body: name (required), templateName (optional, a Workspace or marketplace template) and parentFolderId (optional). Returns 201 with the new site, including id, workspaceId, displayName, shortName, timeZone, previewUrl, customDomains and locales.

Update Site

PATCH /v2/sites/<site_id>

sites:write

Body fields name and parentFolderId, both optional. Returns the updated site.

Delete Site

DELETE /v2/sites/<site_id>

sites:write

Returns 204 with no body.

Get Site Plan

GET /v2/sites/<site_id>/plan

sites:read

Returns the hosting plan’s id and name and a link to plan information.

The response of Create Site includes dataCollectionEnabled and dataCollectionType (always, optOut or disabled), which mirror the site’s privacy settings, so a provisioning script can verify that a new site starts with the setting the organisation’s policy requires and fix it before anything is published.

Fernbrook’s provisioning script creates the sister arboretum’s site from the garden’s template, so that the design system described in Building a Design System starts in place:

// provision-site.ts (run by an Enterprise administrator's automation)
const workspaceId = process.env.WEBFLOW_WORKSPACE_ID!;
const token = process.env.WEBFLOW_API_TOKEN!;

const response = await fetch(
  `https://api.webflow.com/v2/workspaces/${workspaceId}/sites`,
  {
    method: "POST",
    headers: {
      authorization: `Bearer ${token}`,
      "content-type": "application/json",
      accept: "application/json",
    },
    body: JSON.stringify({
      name: "Fernbrook Arboretum",
      templateName: "fernbrook-base-template",
    }),
  },
);

if (!response.ok) {
  throw new Error(`Create Site failed: ${response.status} ${await response.text()}`);
}

const site = await response.json();
console.log(site.id, site.shortName, site.timeZone);

Deleting a Site is a destructive and immediate operation. Treat it like a database drop: restrict sites:write to a dedicated token, require a second human approval in the pipeline that calls it, and take a backup first (see Backups and Recovery).

Site configuration endpoints

The Site configuration endpoints are the API form of settings that a site owner normally edits in Site settings. They matter at Enterprise scale because a federation of sites needs the same baseline everywhere, and a script can apply it and detect drift.

301 redirects

Redirects send a retired path to its replacement (see Redirects and Site Migration for the Dashboard side). The API manages the 301 rules of one site:

Operation Request Scope

List

GET /v2/sites/<site_id>/redirects

sites:read

Create

POST /v2/sites/<site_id>/redirects

sites:write

Update

PATCH /v2/sites/<site_id>/redirects/<redirect_id>

sites:write

Delete

Delete 301 redirects endpoint

sites:write

A rule has an id, a fromUrl (the source path) and a toUrl (the destination path). The list response returns the rules under redirects with a pagination object of limit, offset and total. A create request carries one rule at a time; there is no documented bulk create, so a migration of hundreds of old addresses is a loop that respects the rate limit.

Fernbrook retired the old site’s /gardens section in favour of the Zones collection. The migration script reads the old paths from a CSV file and creates one rule for each:

// create-redirects.ts
import { readFileSync } from "node:fs";

const siteId = process.env.WEBFLOW_SITE_ID!;
const token = process.env.WEBFLOW_API_TOKEN!;
const pairs = readFileSync("old-to-new.csv", "utf8")
  .trim()
  .split("\n")
  .slice(1) // skip the header row
  .map((line) => line.split(","));

for (const [fromUrl, toUrl] of pairs) {
  const response = await fetch(`https://api.webflow.com/v2/sites/${siteId}/redirects`, {
    method: "POST",
    headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
    body: JSON.stringify({ fromUrl, toUrl }),
  });

  if (response.status === 429) {
    const wait = Number(response.headers.get("retry-after") ?? "60");
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
    pairs.push([fromUrl, toUrl]); // retry later
    continue;
  }
  if (!response.ok) {
    console.error(`Failed ${fromUrl}: ${response.status}`);
  }
}

The remaining operations are documented on their own pages: Get 301 redirects, Update 301 redirect and Delete 301 redirects. Redirect paths are site-relative; remember that a redirect that points at a path which is itself redirected creates a chain that search engines may follow only partly.

robots.txt

The robots.txt endpoints read and write the structured rules that Webflow renders into /robots.txt. Each rule names a userAgent and lists allows and disallows; a separate sitemap field holds the sitemap URL.

Operation Request Scope

Get

GET /v2/sites/<site_id>/robots_txt

site_config:read

Replace

PUT /v2/sites/<site_id>/robots_txt

site_config:write

Update

PATCH /v2/sites/<site_id>/robots_txt

site_config:write

Delete

DELETE /v2/sites/<site_id>/robots_txt

site_config:write

The Replace page documents PUT as replacing the stored configuration with the body, which is the safe choice for a baseline that must be identical on every site. Fernbrook keeps one baseline for its federation: crawlers may read everything except the volunteers area, and the sitemap points to the site’s own domain.

{
  "rules": [
    {
      "userAgent": "*",
      "allows": ["/"],
      "disallows": ["/volunteers"]
    },
    {
      "userAgent": "GPTBot",
      "disallows": ["/volunteers", "/staff-only"]
    }
  ],
  "sitemap": "https://www.fernbrook.example/sitemap.xml"
}
curl --request PUT \
  --url "https://api.webflow.com/v2/sites/$WEBFLOW_SITE_ID/robots_txt" \
  --header "authorization: Bearer $WEBFLOW_API_TOKEN" \
  --header 'content-type: application/json' \
  --data @robots-baseline.json

A robots.txt file is advice to well-behaved crawlers, not access control. The volunteers area is protected by password protection (see Password Protection and Site Search), and listing a path under disallows also tells everyone who reads the file that the path exists. The Dashboard side of the same setting is described in the Help Center article Set robots.txt rules.

Well-known files

The well-known endpoint publishes small files under /.well-known/ on the site’s domain. Typical uses are security.txt for vulnerability reports, apple-app-site-association and assetlinks.json for app links, and domain-verification files requested by third-party services.

Operation Request Scope

Set a file

PUT /v2/sites/<site_id>/well_known

site_config:write

Delete files

DELETE /v2/sites/<site_id>/well_known

site_config:write

The body of the set operation has fileName, fileData and an optional contentType of application/json (the default) or text/plain. The documented limits are that each file must be under 100 KB and that a site holds fewer than 30 well-known files. File names may end in .txt or .json, or have no extension. Because some upload tools cannot send an extensionless name, the special .noext marker is stripped when the file is saved: apple-app-site-association.noext.txt is served as apple-app-site-association. A successful set returns 201 and a successful delete returns 204; the delete body names the files in a fileNames list.

// set-security-txt.ts
const siteId = process.env.WEBFLOW_SITE_ID!;
const token = process.env.WEBFLOW_API_TOKEN!;

const securityTxt = [
  "Contact: mailto:security@fernbrook.example",
  "Expires: 2027-10-28T00:00:00Z",
  "Preferred-Languages: en, es, de",
].join("\n");

const response = await fetch(`https://api.webflow.com/v2/sites/${siteId}/well_known`, {
  method: "PUT",
  headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
  body: JSON.stringify({
    fileName: "security.txt",
    fileData: securityTxt,
    contentType: "text/plain",
  }),
});

console.log(response.status); // 201 on success

The delete operation is documented at Data API: Delete a well-known file. Publish the site to make a change visible; the Data API publish endpoint is covered in Data API: Sites, Pages and Components.

People and access

The access endpoints are the API form of the Workspace and Site member screens. Used well, they replace a quarterly spreadsheet of "who can do what" with a script that reads the truth.

Workspace members

Operation Request Scope

List members and invites

GET /v2/workspaces/<workspace_id>/members

workspace_members:read

Invite a member

POST /v2/workspaces/<workspace_id>/members

workspace_members:write

Update a member’s role

Update Workspace Member Role endpoint

workspace_members:write

Remove a member

Remove Workspace Member endpoint

workspace_members:write

Bulk update roles

POST /v2/workspaces/<workspace_id>/members/bulk

workspace_members:write

Bulk remove members

Bulk Remove Workspace Members endpoint

workspace_members:write

The list endpoint returns both members and invites (pending, declined and expired) and filters by email and by roles; its default page size is 50 with a maximum of 100, and paging uses the nextCursor of the previous response. The limit is applied to the two lists separately. A member has a workspace role (owner, admin, member, guest, reviewer or client) and, for Workspaces on Workspace seats, a default Site role.

An invite is one of three shapes: a default Site role invite (the person gets one Site role across the Workspace’s sites), a Site access invite (a list of sites, all with the same Site role) and a collaborator client invite (one site only). Role pairings follow the Dashboard’s role picker: an admin workspace role pairs with the site_manager default role, a member pairs with site_manager, designer, marketer, content_editor or custom, and a reviewer pairs with reviewer. The error codes include insufficient_seats, user_limit_reached and member_already_has_access, which a script should treat as expected, recoverable outcomes.

// invite-editor.ts
const workspaceId = process.env.WEBFLOW_WORKSPACE_ID!;
const token = process.env.WEBFLOW_API_TOKEN!;

const response = await fetch(
  `https://api.webflow.com/v2/workspaces/${workspaceId}/members`,
  {
    method: "POST",
    headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
    body: JSON.stringify({
      email: "new.volunteer@example.org",
      workspaceRole: "member",
      defaultSiteRole: "content_editor",
    }),
  },
);

const result = await response.json();
console.log(response.status, result.status); // "invited" or "active"

The bulk role endpoint takes a role, a defaultSiteRole and lists of memberIds and inviteIds. It returns 200 as soon as the batch has been applied, even if every item failed, so read summary (updated and failed) and the per-item results; results are de-duplicated, so match them to the input by id, not by position. One failure code, workspace_member_managed_by_scim, shows that members whose lifecycle is controlled by SCIM cannot be changed by hand or by this API.

Site access

Site access narrows or widens one member’s rights on one Site.

Operation Request Scope

List Site members

GET /v2/sites/<site_id>/members

site_access:read

Grant access

POST /v2/sites/<site_id>/members

site_access:write

Change a Site role

Update Site Member Role, by member ID or by email

site_access:write

Revoke access

Revoke Site Access, by member ID or by email

site_access:write

The grant endpoint creates new access and never modifies an existing grant: if the member already has access it fails with a 409 whose code is member_already_has_access, and the way to change a role is the update endpoint. The body has a required siteRole (site_manager, designer, marketer, content_editor, reviewer or custom), either an email or a memberId, and a siteRoleId that is required when the role is custom and rejected otherwise. In responses a Site role can also be branch_designer, limited_designer or viewer.

curl --request POST \
  --url "https://api.webflow.com/v2/sites/$WEBFLOW_SITE_ID/members" \
  --header "authorization: Bearer $WEBFLOW_API_TOKEN" \
  --header 'content-type: application/json' \
  --data '{"email": "journal.editor@example.org", "siteRole": "content_editor"}'

The list endpoint pages with limit (default 50, maximum 100), email and cursor, and is the building block of an access review: Data API: List Site Members.

Audit trails

Webflow keeps two different logs, aimed at two different questions. Do not confuse them.

Log Question it answers Endpoint and scope

Workspace audit log

Who signed in, who was added, removed or re-roled, which custom role changed, which Workspace setting was toggled?

GET /v2/workspaces/<workspace_id_or_slug>/audit_logs, workspace_activity:read, Workspace token.

Site Activity log

What changed on this Site: CMS items, pages, publishes, branches; by which person, agent or system?

GET /v2/sites/<site_id>/activity_logs, site_activity:read.

The Site Activity log is also a Dashboard panel and exists in lighter form outside Enterprise; the panel and its history are described in Collaboration, Comments and the Activity Log. The API is the Enterprise part.

Workspace audit log

The request takes a limit (at most 100), an offset, a sortOrder (asc or desc), a from and a to (ISO 8601 timestamps) and an eventType filter. The response has an items array and a pagination object with limit, offset and total. Every event has an eventType, an eventSubType, a payload and an actor that is either a person (with an email) or a token (with a name), so an action taken by an API token is attributable to that token.

eventType eventSubType values

user_access

login, logout

custom_role

role_created, role_updated, role_deleted

workspace_membership

user_added, user_removed, user_role_updated

site_membership

user_added, user_removed, user_role_updated, user_granular_access_updated

workspace_invitation

invite_sent, invite_accepted, invite_role_updated, invite_canceled, invite_declined, access_request_accepted, access_request_declined

workspace_setting

setting_updated (currently the AI enablement setting only)

The payload depends on the type. For user_access it records the sign-in method (dashboard, sso, api or google), a coarse location and the ipAddress. Membership and invitation payloads carry the targetUser, the userType (member, guest, reviewer or client), the new roleName and the previousRoleName, and a method whose values include sso and scim as well as dashboard, admin, invite, access_request and api. That method is how a reviewer tells a person added by a SCIM sync from one added by a Workspace admin in the Dashboard.

A trimmed example of what a membership event looks like, with placeholder values:

{
  "eventType": "workspace_membership",
  "eventSubType": "user_role_updated",
  "actor": { "type": "person", "id": "user_placeholder_1", "email": "admin@example.org" },
  "payload": {
    "targetUser": { "id": "user_placeholder_2", "email": "volunteer@example.org" },
    "method": "dashboard",
    "userType": "member",
    "roleName": "Designer",
    "previousRoleName": "Content editor"
  }
}

The documented SDK call uses webflow.workspaces.auditLogs.getWorkspaceAuditLogs in JavaScript and client.workspaces.audit_logs.get_workspace_audit_logs in Python:

import { WebflowClient } from "webflow-api";

const webflow = new WebflowClient({ accessToken: process.env.WEBFLOW_WORKSPACE_TOKEN! });

const logs = await webflow.workspaces.auditLogs.getWorkspaceAuditLogs("fernbrook-workspace", {
  from: new Date("2026-10-01T00:00:00Z"),
  to: new Date("2026-10-28T00:00:00Z"),
  limit: 100,
});

for (const entry of logs.items ?? []) {
  console.log(entry.eventType, entry.eventSubType);
}
import datetime
import os

from webflow.client import Webflow

client = Webflow(access_token=os.environ["WEBFLOW_WORKSPACE_TOKEN"])

audit_logs = client.workspaces.audit_logs.get_workspace_audit_logs(
    workspace_id_or_slug="fernbrook-workspace",
    from_=datetime.datetime(2026, 10, 1, tzinfo=datetime.timezone.utc),
    to=datetime.datetime(2026, 10, 28, tzinfo=datetime.timezone.utc),
)
print(audit_logs)

Site Activity log

The Site Activity endpoint takes only limit (maximum 100) and offset. Each entry carries an id, createdOn and lastUpdated, an event (for example cms_item, site_published, page_created or branch_merged), a resourceOperation (CREATED, MODIFIED, PUBLISHED, UNPUBLISHED, DELETED and reordering and group variants), the acting user (an id and a displayName), resourceId, resourceName, newValue, previousValue and an event-specific payload.

Two fields were added with the AI and MCP attribution described in MCP Server and AI Agents. source names the originating system: WEBFLOW_AI, WEBFLOW_MCP, DESIGNER or SYSTEM. actorType is user, agent, workflow or rule, with actorId and actorName. Both are null for events recorded before attribution existed. Governance teams use them to answer "did a person or an agent publish this change?", which is one of the strongest reasons for the Fernbrook trustees to read this log. The documentation does not state a retention period, so a SIEM that must keep history for years should copy entries out as they are produced instead of relying on the API as the archive.

curl --request GET \
  --url "https://api.webflow.com/v2/sites/$WEBFLOW_SITE_ID/activity_logs?limit=100&offset=0" \
  --header 'accept: application/json' \
  --header "authorization: Bearer $WEBFLOW_API_TOKEN"

Feeding audit logs to a SIEM

The audit endpoints are pull endpoints, so a forwarder polls on a schedule and ships each new event to the log store. Page-branch events are the exception for Site changes, because the branch-created, branch-merged and branch-deleted webhooks exist (see Webhooks).

sequenceDiagram participant Cron as Scheduler participant Fwd as Forwarder participant WF as Webflow API participant State as Checkpoint store participant SIEM as SIEM Cron->>Fwd: every 5 minutes Fwd->>State: read last timestamp Fwd->>WF: GET audit_logs from last timestamp, limit 100 WF-->>Fwd: items and pagination loop until offset reaches total Fwd->>WF: next page end Fwd->>SIEM: send events with an idempotency key Fwd->>State: write newest timestamp

A robust forwarder follows a few rules:

  • Checkpoint by time, de-duplicate by content. Use the from filter with the newest timestamp already shipped, re-read a small overlap, and drop events whose key (type, subtype, actor, target and timestamp) has already been stored. The audit events have no single documented identifier, so a derived key is required.

  • Page with offset until it reaches total. Sort ascending so a crash in the middle leaves a continuous prefix.

  • Respect the limits. On a 429, wait for Retry-After and keep the checkpoint where it was.

  • Protect the token. The forwarder holds a read-only workspace_activity:read token in a secret store. Revoke and replace it on a schedule.

  • Alert on the interesting subset. Role changes to Admin, custom_role events, workspace_setting events, sign-ins from unexpected places, and Site Activity entries with actorType of agent that publish a site.

// forward-audit-logs.ts
import { WebflowClient } from "webflow-api";

const webflow = new WebflowClient({ accessToken: process.env.WEBFLOW_WORKSPACE_TOKEN! });
const workspace = process.env.WEBFLOW_WORKSPACE_SLUG!;

async function* pull(from: Date) {
  let offset = 0;
  while (true) {
    const page = await webflow.workspaces.auditLogs.getWorkspaceAuditLogs(workspace, {
      from,
      limit: 100,
      offset,
    });
    const items = page.items ?? [];
    yield* items;
    offset += items.length;
    if (items.length === 0 || offset >= (page.pagination?.total ?? 0)) return;
  }
}

const checkpoint = new Date(process.env.LAST_CHECKPOINT ?? "2026-10-01T00:00:00Z");
for await (const event of pull(checkpoint)) {
  await fetch(process.env.SIEM_INGEST_URL!, {
    method: "POST",
    headers: { "content-type": "application/json", authorization: `Bearer ${process.env.SIEM_TOKEN}` },
    body: JSON.stringify(event),
  });
}

The full pipeline belongs in version control and in the CI conventions of Testing, CI and DevOps; a contract test that replays a recorded page of audit events through the forwarder catches API drift before a gap appears in the evidence.

Identity, SSO and SCIM

Enterprise Workspaces can sign members in through the organisation’s identity provider and can have their member lifecycle provisioned by SCIM. Both are configured by Workspace administrators in the Dashboard together with Webflow, not through the Data API, and both leave traces in the API that are useful to know.

  • Single sign-on. The sign-in method of a user_access event is sso when a member authenticated through the identity provider, so the audit log can show how many sign-ins bypass it, for example dashboard or google sign-ins that an organisation intends to retire.

  • SCIM provisioning. Membership and invitation events carry scim as a method when the identity provider created or changed them, and a member managed by SCIM cannot be re-roled by the bulk role endpoint (the failure code is workspace_member_managed_by_scim). The identity provider is therefore the source of truth for who is in the Workspace, and the API is for reading and for the cases the provider does not cover.

  • Offboarding. When a person leaves the organisation, disabling them in the identity provider is what ends their Webflow access. The audit log’s user_removed events with method scim are the evidence that it did.

The configuration screens, supported identity providers and licensing terms are described by Webflow for Enterprise customers and can change between releases, so follow the current Webflow documentation for them rather than the Data API reference.

Custom roles

The built-in roles cover the common cases (see Plans, Workspaces, Seats and Roles for the table and the seat model). An Enterprise Workspace can additionally define custom roles, up to 20, starting from the permissions of a built-in role and adjusting them. The API surface for them is small and worth knowing precisely:

  • A custom Site role is referenced by siteRole: "custom" plus a siteRoleId in the grant, update, invite and bulk endpoints; the ID is required with custom and rejected with any other role.

  • In responses the member’s roles.site is custom and roles.siteRoleId holds the role’s ID.

  • Creating, changing and deleting a role are written to the Workspace audit log as custom_role events with role_created, role_updated and role_deleted, together with the role name and its previous name.

  • Granular access changes on a Site (restricting a role to certain resources) appear as site_membership events with user_granular_access_updated and a granularAccess object holding the resource’s id, name, type and a restricted flag.

The Fernbrook web team defines a Journal contributor custom role that can edit Journal items and upload assets but cannot publish, and grants it with siteRole: "custom". The audit log answers the trustees' question of who created the role and when.

{
  "email": "journal.volunteer@example.org",
  "siteRole": "custom",
  "siteRoleId": "role_id_placeholder"
}

Team plan and the Enterprise gap

The Team plan (a Site plan tier introduced in 2026 and described in Plans, Workspaces, Seats and Roles) moves several collaboration features that used to be Enterprise-only down a tier: page branching, single-page publishing, the publishing workflow and the Site Activity log in the Dashboard. The Enterprise reference sections described here are documented as requiring an Enterprise workspace. The practical consequences for a developer are:

  • A Team site can use the Data API as any paid site does, including webhooks, CMS and publish endpoints, but the Enterprise endpoints for creating or deleting Sites, managing redirects, robots.txt or well-known files, managing members and reading the audit logs are not part of it. Those tasks are done in the Dashboard.

  • Page branching is usable from the Designer on a Team site, and the branch-created, branch-merged and branch-deleted webhook payloads describe branches (siteId, branchId, pageId, parentPageId, pageName, createdBy and createdOn). The branch-management scopes (branches:read, branches:create, branches:update, branches:delete, branches:merge, branches:publish) are listed in the Scopes reference. See Page Branching, Releases and Approvals.

  • The Developer workspace plan, which is free and limited to three workspace users and 1,000 visitors a month, includes Custom Code, the Site Activity Log and Page Branching, and does not allow publishing to production, code export or site transfer. It is therefore a good place to rehearse the Site Activity calls.

A governance checklist for a federation of sites

A short operating model for an organisation such as Fernbrook Gardens, in the order a new Enterprise Workspace usually needs it:

  1. Identity first. Connect the identity provider for sign-in and provisioning so that joiners and leavers follow HR records, and keep a small number of break-glass administrator accounts whose use alerts the web team.

  2. Roles second. Prefer built-in roles; add custom roles only where a real separation of duties exists, such as editors who cannot publish. Review them through site_membership and custom_role events.

  3. Tokens third. Create one workspace token for each integration with the minimum scope, store the values in a secrets manager, rotate them, and revoke them when an owner leaves. Remember the five-token limit when planning.

  4. Baselines as code. Keep the robots rules, the security file and the redirect list of each site in version control and apply them with the endpoints above on every release. A nightly job lists the live values and reports drift.

  5. Evidence continuously. Forward both logs to the SIEM and retain them according to the organisation’s policy, because the API does not state a retention period.

  6. Review quarterly. A script lists members and invites with the list endpoints, flags people with no sign-in in 90 days from the audit log and sends the report to site owners to confirm or revoke.

  7. Restrict destructive calls. Site deletion and bulk member removal belong in a pipeline with a second approver and a fresh backup.

Pitfalls and corrections

  • Plan errors are not scope errors. An Enterprise-only call from a non-Enterprise workspace fails with a plan-related error even when the token has every scope; check the Workspace plan before debugging the token.

  • Site tokens cannot do Workspace administration. The Workspace audit log needs a Workspace token or a Data Client app registered with workspace-level access; a site token returns an authorisation error. New site tokens cannot have the workspace:read scope.

  • Documentation inconsistencies. A few endpoint pages show example fields that differ slightly from their schema (for example the Site Plan response names its information link pricingInfo in the schema and pricingDetails in the example). Read the field defensively and prefer the schema.

  • Bulk endpoints return 200 on partial failure. Always inspect summary and results.

  • Publish after changing configuration. Publish the site after changing redirects, robots.txt or well-known files, and fetch the live file to confirm the result.

  • Older material. Older books and tutorials describe these tasks in the Dashboard only. The reference now documents each of them as an endpoint with its scope. See Where Older Books and Tutorials Are Outdated.

Where to go next