Skip to main content

Overview

Adapt integrates directly with Webflow using OAuth 2.0 authentication and publish webhooks. This enables automatic cache warming and site health monitoring whenever you publish changes to your Webflow sites. Key Features:
  • OAuth 2.0 authentication with Webflow workspaces
  • Automatic webhook registration per site
  • Publish-triggered crawls for instant cache warming
  • Per-site scheduling configuration
  • Secure token storage in Supabase Vault

OAuth Flow

Initiating Connection

Start the OAuth flow by making a POST request to initiate the connection:
POST /v1/integrations/webflow
Authorization: Bearer <your_jwt_token>
Response:
{
  "status": "success",
  "data": {
    "auth_url": "https://webflow.com/oauth/authorize?client_id=..."
  },
  "message": "Redirect to this URL to connect Webflow"
}
Redirect the user to the auth_url to complete authorization.

Required OAuth Scopes

Adapt requests the following scopes from Webflow:
  • authorized_user:read - Identify the connecting user
  • sites:read - List sites in the workspace
  • sites:write - Register webhooks for publish events
  • cms:read - Access CMS content for crawling
The workspaces:read scope is Enterprise-only, so Adapt uses authorized_user:read to identify connections.

OAuth Callback

Webflow redirects back to:
https://your-domain.com/v1/integrations/webflow/callback?code=xxx&state=xxx
Adapt handles this automatically and:
  1. Validates the OAuth state parameter (HMAC-signed with SUPABASE_JWT_SECRET)
  2. Exchanges the authorization code for an access token
  3. Fetches workspace and user information
  4. Stores the connection and access token securely
  5. Redirects to your settings page with success status

Implementation Reference

From internal/api/auth_webflow.go:99-108:
// Build Webflow OAuth URL
authURL := fmt.Sprintf(
    "https://webflow.com/oauth/authorize?client_id=%s&response_type=code&scope=%s&redirect_uri=%s&state=%s",
    url.QueryEscape(getWebflowClientID()),
    url.QueryEscape(scopes),
    url.QueryEscape(getWebflowRedirectURI()),
    url.QueryEscape(state),
)

Connection Management

List Connections

Retrieve all Webflow connections for your organisation:
GET /v1/integrations/webflow
Authorization: Bearer <your_jwt_token>
Response:
{
  "status": "success",
  "data": [
    {
      "id": "conn_abc123",
      "webflow_workspace_id": "workspace_xyz",
      "workspace_name": "John Doe",
      "created_at": "2024-03-03T12:00:00Z"
    }
  ]
}

Delete Connection

DELETE /v1/integrations/webflow/{connection_id}
Authorization: Bearer <your_jwt_token>
This removes the connection and deletes the stored access token from the Vault.

Site Configuration

List Sites

Fetch all sites in a connected workspace:
GET /v1/integrations/webflow/{connection_id}/sites
Authorization: Bearer <your_jwt_token>
Response:
{
  "status": "success",
  "data": {
    "sites": [
      {
        "id": "site_123",
        "display_name": "My Portfolio",
        "short_name": "portfolio",
        "custom_domains": [
          {
            "url": "https://example.com"
          }
        ],
        "workspace_id": "workspace_xyz"
      }
    ]
  }
}

Configure Site Settings

Enable or disable auto-crawl on publish for a specific site:
PUT /v1/integrations/webflow/{connection_id}/sites/{site_id}/settings
Authorization: Bearer <your_jwt_token>
Content-Type: application/json

{
  "enabled": true,
  "schedule_interval_hours": 24,
  "concurrency": 20,
  "find_links": true,
  "max_pages": 0
}
1

Webhook Registration

When enabled: true, Adapt automatically registers a webhook with Webflow for site_publish events.
2

Scheduler Creation

If schedule_interval_hours is provided, a recurring scheduler is created for the site.
3

Token Storage

The webhook ID is stored in your database for future management.
Response:
{
  "status": "success",
  "data": {
    "site_id": "site_123",
    "enabled": true,
    "schedule_interval_hours": 24,
    "webhook_id": "webhook_xyz",
    "webhook_registered_at": "2024-03-03T12:30:00Z"
  }
}

Publish Webhooks

Webhook URL Format

Adapt registers webhooks with Webflow using workspace-scoped URLs:
https://your-domain.com/v1/webhooks/webflow/workspaces/{workspace_id}
This format allows multiple organisations to use the same workspace by mapping the workspace ID to the correct organisation.

Webhook Payload

When you publish a site, Webflow sends:
{
  "site": "site_123",
  "workspaceId": "workspace_xyz",
  "publishTime": "2024-03-03T14:45:00Z",
  "triggerType": "site_publish"
}

Processing Logic

From internal/api/handlers.go:916-985, Adapt:
  1. Parses the webhook payload - Extracts site ID and workspace ID
  2. Resolves the organisation - Maps workspace ID to organisation using platform_org_mapping
  3. Checks site settings - Verifies auto-publish is enabled for the site
  4. Creates a job - Starts a new crawl job with configured settings
  5. Returns 200 OK - Acknowledges receipt to Webflow
Webhooks are unauthenticated public endpoints. Adapt validates the workspace ID against known mappings to prevent abuse.

Webhook Registration Code

From internal/api/webflow_sites.go:763-811:
func (h *Handler) registerWebflowWebhook(ctx context.Context, token, siteID, webhookURL string) (string, error) {
    payload := map[string]string{
        "url":         webhookURL,
        "triggerType": "site_publish",
    }
    
    reqURL := fmt.Sprintf("https://api.webflow.com/v2/sites/%s/webhooks", siteID)
    
    // ... HTTP request implementation ...
    
    return webhookResp.ID, nil
}

Handling Conflicts

If a webhook already exists, Adapt:
  1. Lists existing webhooks for the site
  2. Finds the matching webhook by URL and trigger type
  3. Returns the existing webhook ID
This ensures idempotent webhook registration.

Security

Token Storage

Access tokens are stored in Supabase Vault (encrypted at rest):
if err := h.DB.StoreWebflowToken(r.Context(), conn.ID, tokenResp.AccessToken); err != nil {
    // Handle error - cleanup orphan connection
}
From internal/api/auth_webflow.go:186-195.

State Validation

OAuth state parameters are HMAC-signed using SUPABASE_JWT_SECRET to prevent CSRF attacks:
state, err := h.generateOAuthState(userClaims.UserID, orgID)
The state includes user ID and organisation ID, validated on callback.

Organisation Isolation

All operations are scoped to the authenticated user’s organisation via Row Level Security (RLS) in PostgreSQL.

Environment Variables

# Required for OAuth
WEBFLOW_CLIENT_ID=your_client_id
WEBFLOW_CLIENT_SECRET=your_client_secret
WEBFLOW_REDIRECT_URI=https://your-domain.com/v1/integrations/webflow/callback

# Required for state signing
SUPABASE_JWT_SECRET=your_jwt_secret

# App URL for webhook registration
APP_URL=https://your-domain.com

Common Issues

Webhook Registration Fails

Symptom: Error when enabling auto-publish Causes:
  • Missing sites:write scope
  • Invalid access token (expired or revoked)
  • Webflow API rate limits
Solution: Reconnect the workspace to refresh scopes and token.

Workspace ID Missing

Symptom: Connection saved but webhook callbacks fail Cause: Webflow token introspection didn’t return workspace IDs Solution: Adapt falls back to extracting workspace IDs from the sites API. Ensure the connection has at least one site.

Multiple Organisations

Symptom: Webhook triggers crawl for wrong organisation Cause: Missing or incorrect platform_org_mapping Solution: Verify the workspace ID is correctly mapped to your organisation in the database.

API Reference

EndpointMethodDescription
/v1/integrations/webflowPOSTInitiate OAuth flow
/v1/integrations/webflowGETList connections
/v1/integrations/webflow/{id}DELETERemove connection
/v1/integrations/webflow/{id}/sitesGETList sites
/v1/integrations/webflow/{id}/sites/{site_id}/settingsPUTConfigure site
/v1/webhooks/webflow/workspaces/{workspace_id}POSTWebhook endpoint

Next Steps

Slack Notifications

Get notified when crawls complete

Custom Webhooks

Send crawl results to your systems

Build docs developers (and LLMs) love