Quick reference for the most common Webflow Data API v2 errors, their root causes, and concrete solutions. Covers every HTTP status code the API returns.
webflow-api SDK installed{ "code": "validation_error", "message": "Invalid field data" }
Common causes:
name and slug are always required for CMS items)Fix:
// Check collection schema before creating items
const collection = await webflow.collections.get(collectionId);
const requiredFields = collection.fields?.filter(f => f.isRequired);
console.log("Required fields:", requiredFields?.map(f => `${f.slug} (${f.type})`));
// Validate slug format
function isValidSlug(slug: string): boolean {
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
}
{ "code": "unauthorized", "message": "Not authorized" }
Common causes:
Fix:
# Verify token works
curl -s https://api.webflow.com/v2/sites \
-H "Authorization: Bearer $WEBFLOW_API_TOKEN" \
-w "\nHTTP Status: %{http_code}\n"
# Check for whitespace issues
echo -n "$WEBFLOW_API_TOKEN" | wc -c
// Programmatic token check
async function verifyToken(): Promise<boolean> {
try {
await webflow.sites.list();
return true;
} catch (err: any) {
if (err.statusCode === 401) {
console.error("Token invalid. Generate new token at developers.webflow.com");
return false;
}
throw err;
}
}
{ "code": "forbidden", "message": "Insufficient permissions" }
Common causes:
cms:read)Fix:
// Required scopes by operation
const SCOPE_MAP: Record<string, string> = {
"sites.list": "sites:read",
"sites.publish": "sites:write",
"collections.list": "cms:read",
"collections.items.createItem": "cms:write",
"pages.list": "pages:read",
"forms.list": "forms:read",
"products.list": "ecommerce:read",
"products.create": "ecommerce:write",
"orders.list": "ecommerce:read",
"orders.refund": "ecommerce:write",
};
Generate a new token with the correct scopes at https://developers.webflow.com.
{ "code": "not_found", "message": "Resource not found" }
Common causes:
site_id, collection_id, or item_id
Fix:
// Discovery chain: always start from sites.list()
async function discoverResources() {
const { sites } = await webflow.sites.list();
console.log("Sites:", sites?.map(s => `${s.displayName}: ${s.id}`));
for (const site of sites!) {
const { collections } = await webflow.collections.list(site.id!);
console.log(` Collections in ${site.displayName}:`);
for (const col of collections!) {
console.log(` ${col.displayName}: ${col.id}`);
}
}
}
{ "code": "conflict", "message": "Item with slug already exists" }
Common causes:
Fix:
// Check for existing slug before creating
async function createOrUpdate(collectionId: string, slug: string, fieldData: Record<string, any>) {
const { items } = await webflow.collections.items.listItems(collectionId);
const existing = items?.find(i => i.fieldData?.slug === slug);
if (existing) {
return webflow.collections.items.updateItem(collectionId, existing.id!, { fieldData });
}
return webflow.collections.items.createItem(collectionId, { fieldData: { slug, ...fieldData } });
}
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Common causes:
Fix:
// The SDK auto-retries 429s with exponential backoff.
// For manual control:
async function waitForRateLimit(retryAfterSeconds: number) {
console.log(`Rate limited. Waiting ${retryAfterSeconds}s...`);
await new Promise(r => setTimeout(r, retryAfterSeconds * 1000));
}
See webflow-rate-limits for comprehensive rate limit handling.
Fix:
# 1. Check Webflow status
curl -s https://status.webflow.com/api/v2/status.json | jq '.status'
# 2. Retry with backoff (SDK handles this automatically)
# 3. If persistent, check Webflow status page and open support ticket
# Test API connectivity
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $WEBFLOW_API_TOKEN" \
https://api.webflow.com/v2/sites
# Check rate limit headers
curl -v -H "Authorization: Bearer $WEBFLOW_API_TOKEN" \
https://api.webflow.com/v2/sites 2>&1 | grep -i "x-ratelimit\|retry-after"
# List sites (quick token verification)
curl -s -H "Authorization: Bearer $WEBFLOW_API_TOKEN" \
https://api.webflow.com/v2/sites | jq '.sites[].displayName'
# Check Webflow platform status
curl -s https://status.webflow.com/api/v2/status.json | jq '.status.description'
import { WebflowClient } from "webflow-api";
async function resilientCall<T>(
operation: () => Promise<T>,
label: string
): Promise<T> {
try {
return await operation();
} catch (err: any) {
const status = err.statusCode || err.status;
const actionMap: Record<number, string> = {
400: "Fix request payload — check field names and types",
401: "Rotate token at developers.webflow.com",
403: "Add missing scope to token",
404: "Verify resource IDs with discovery chain",
409: "Handle duplicate — update instead of create",
429: "Wait for Retry-After header (SDK auto-retries)",
500: "Webflow server error — retry later",
};
console.error(`[${label}] HTTP ${status}: ${actionMap[status] || "Unknown error"}`);
console.error(` Message: ${err.message}`);
console.error(` Body: ${JSON.stringify(err.body)}`);
throw err;
}
}
webflow-debug-bundle
For comprehensive debugging, see webflow-debug-bundle.