When the Webflow API seems to drop a collection field, the cause is usually an empty value or a display name used in place of a slug, and a schema diff shows which.
When pulling collection items from the Webflow API, you might notice that certain fields are missing from the response payload. A field shows in the CMS panel, your Next.js app lists the collection's items, and the key you expected is absent from fieldData.
The API usually isn't dropping it. Items with no stored value come back without that key, and fieldData is keyed by field slug, not display name. Live reads add two more gaps, since /items/live returns only published content and the Content Delivery API caches responses for up to five minutes.
In this guide, we’ll build a Route Handler on Webflow Cloud that diffs the collection schema against staged and live items, then a normalizer that hands your front end every schema field on every item.
What do you need to fix missing collection fields in the Webflow API?
You need to confirm that you have these five prerequisites:
- A Webflow site: On the Premium Site plan or higher, which includes 20,000 CMS items and the custom domain mount Webflow Cloud needs.
- A Next.js 15 or higher app: With an
appdirectory for the Route Handlers, as listed on the bring-your-own-app page, running locally on Node.js 22 or later. - npm: Webflow Cloud supports only npm, so start from an npm project with a
package-lock.json. - A site token: With the
cms:readscope, which is all this tutorial needs, plus the ID of the collection whose fields go missing. - A terminal with curl: To call the audit route locally and after deployment.
Once you have these prerequisites, you are ready to begin implementation.
5 steps to return every collection field from the Webflow API in Webflow Cloud
The build is a shared API client, an audit route that diffs the collection schema against staged and live items, and a normalizer for production. The diff comes first, because it turns "fields are missing" into a list of exact slugs and item IDs.
1. Create a read-only site token and store it in Webflow Cloud
Generate a site token in Site settings under Apps & integrations and grant only CMS read access, which limits the damage if the token leaks. Then copy the collection's ID from its settings in the CMS. If you haven't worked with tokens and collection IDs before, Webflow's guide to the CMS API walks you through both.
Add three variables to .env.local for local work, and the same three to your Webflow Cloud environment.
The code in later steps reads them exactly as written:
WEBFLOW_API_TOKEN=your_site_api_token
WEBFLOW_COLLECTION_ID=your_collection_id
AUDIT_KEY=a_long_random_string_you_generate
In Webflow Cloud, mark WEBFLOW_API_TOKEN and AUDIT_KEY as secret. Webflow's environment variables page says both secret and plain variables are available at build time and at runtime, and secret values are redacted from build logs. Add the variables to each environment you deploy to.
2. Add a typed Webflow client that fails loudly
Write one small helper that every route uses to call the Data API. It throws with the status code and the response body whenever a request fails, so a failed request can never look like a missing field.
It also pages through every item, because a list call returns at most 100, and it reads the collection schema from Get Collection, which lists each field's slug beside its displayName.
Save this file as lib/webflow.ts at the project root:
// lib/webflow.ts
export const API_BASE = 'https://api.webflow.com/v2';
export const CDN_BASE = 'https://api-cdn.webflow.com/v2';
export type SchemaField = {
id: string;
slug: string;
displayName: string;
type: string;
};
export type CollectionItem = {
id: string;
fieldData: Record<string, unknown>;
};
export async function webflowGet<T>(base: string, path: string): Promise<T> {
const token = process.env.WEBFLOW_API_TOKEN;
if (!token) {
throw new Error('WEBFLOW_API_TOKEN is not set for this environment');
}
const res = await fetch(`${base}${path}`, {
headers: { Authorization: `Bearer ${token}`, accept: 'application/json' },
cache: 'no-store',
});
if (!res.ok) {
throw new Error(`Webflow API ${res.status} on ${path}: ${await res.text()}`);
}
return (await res.json()) as T;
}
export async function getSchemaFields(collectionId: string): Promise<SchemaField[]> {
const collection = await webflowGet<{ fields: SchemaField[] }>(
API_BASE,
`/collections/${collectionId}`,
);
return collection.fields;
}
export async function listAll(base: string, path: string): Promise<CollectionItem[]> {
const all: CollectionItem[] = [];
const separator = path.includes('?') ? '&' : '?';
let offset = 0;
for (;;) {
const page = await webflowGet<{ items: CollectionItem[]; pagination: { total: number } }>(
base,
`${path}${separator}limit=100&offset=${offset}`,
);
all.push(...page.items);
offset += page.items.length;
if (page.items.length === 0 || offset >= page.pagination.total) return all;
}
}
The open record type for fieldData keeps every key the API returns, including fields added to the collection after you wrote the type. Option fields also carry validations.options in the schema, which you need later if a field holds an option ID.
3. Build an audit route that diffs the schema against each item
Create a Route Handler that loads the schema and reports each slug that is absent from a staged or live item's fieldData. Leave out export const runtime = 'edge', even though the bring-your-own-app page still tells you to add it.
Webflow Cloud runs on Cloudflare Workers, but the Next.js edge runtime target is a separate setting that the OpenNext Cloudflare adapter does not support, so the directive breaks the build.
Put the route at app/api/fields-audit/route.ts.
It is a temporary diagnostic, so delete it once the audit is done:
// app/api/fields-audit/route.ts
import { NextResponse } from 'next/server';
import { API_BASE, getSchemaFields, listAll, type CollectionItem } from '../../../lib/webflow';
async function sha256(value: string): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
}
function findGaps(items: CollectionItem[], slugs: string[]) {
return items
.map((item) => ({
id: item.id,
missing: slugs.filter((slug) => !(slug in item.fieldData)),
}))
.filter((row) => row.missing.length > 0);
}
export async function GET(request: Request) {
const expected = process.env.AUDIT_KEY;
const provided = request.headers.get('x-audit-key') ?? '';
if (!expected || (await sha256(provided)) !== (await sha256(expected))) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const collectionId = process.env.WEBFLOW_COLLECTION_ID;
if (!collectionId) {
return NextResponse.json({ error: 'WEBFLOW_COLLECTION_ID is not set' }, { status: 500 });
}
try {
const fields = await getSchemaFields(collectionId);
const slugs = fields.map((field) => field.slug);
const staged = await listAll(API_BASE, `/collections/${collectionId}/items`);
let live: { count: number; gaps: ReturnType<typeof findGaps> } | { error: string };
try {
const liveItems = await listAll(API_BASE, `/collections/${collectionId}/items/live`);
live = { count: liveItems.length, gaps: findGaps(liveItems, slugs) };
} catch (err) {
live = { error: String(err) };
}
return NextResponse.json({
schema: fields.map(({ slug, displayName, type }) => ({ slug, displayName, type })),
staged: { count: staged.length, gaps: findGaps(staged, slugs) },
live,
});
} catch (err) {
return NextResponse.json({ error: String(err) }, { status: 502 });
}
}
The key check compares SHA-256 digests, which hides the key's length and runs the same under next dev and on Workers, unlike crypto.subtle.timingSafeEqual. The audit reads live items from api.webflow.com rather than the cached delivery host, so it always shows what is published right now.
Running the local audit
Start the dev server with npm run dev, export the same key in a second terminal (export AUDIT_KEY=your_value), and call the route:
curl -H "x-audit-key: $AUDIT_KEY" http://localhost:3000/api/fields-audit
You get back the schema plus the ID of each item that has gaps, with its missing slugs. Read the output against this table:
| Audit output | What to check first |
|---|---|
| A slug is missing on some staged items and present on others | Whether those items have a value for that field in the CMS panel |
| A slug is missing on every staged and live item | Whether any item has a value for that field yet |
| A slug appears in staged items but not in live ones | Whether the items' latest edits have been published |
The live block holds an error instead of a count |
The status code inside the error text |
No gaps at all, but your component still shows undefined |
Whether your code reads a display name or an old key instead of the slug |
| Audit output → What to check first |
|---|
| A slug is missing on some staged items and present on others |
| Whether those items have a value for that field in the CMS panel |
| A slug is missing on every staged and live item |
| Whether any item has a value for that field yet |
| A slug appears in staged items but not in live ones |
| Whether the items' latest edits have been published |
The live block holds an error instead of a count |
| The status code inside the error text |
No gaps at all, but your component still shows undefined |
| Whether your code reads a display name or an old key instead of the slug |
Once one row matches your output, you know whether to look at your content or at your own code.
4. Normalize items so every schema field is present
Fill every schema slug on every item, using null wherever the API supplied nothing. Without that, an item missing a key hands your component undefined instead of a null it can test for, and a front end that expects a stable shape breaks on whichever item happens to be incomplete. Normalizing in the route puts that fix in one place.
Creating the field normalizer
Add the normalizer as lib/normalize.ts:
// lib/normalize.ts
import type { CollectionItem, SchemaField } from './webflow';
export type NormalizedItem = {
id: string;
fields: Record<string, unknown>;
};
export function normalizeItem(item: CollectionItem, schema: SchemaField[]): NormalizedItem {
const fields: Record<string, unknown> = {};
for (const field of schema) {
fields[field.slug] = item.fieldData[field.slug] ?? null;
}
return { id: item.id, fields };
}
The production route serves normalized live items to your front end. It reads them from the Content Delivery API, where cached responses don't count against the Data API's rate limits, and it keeps upstream error details in the logs rather than in its public response.
Creating the production API route
Add it at app/api/items/route.ts:
// app/api/items/route.ts
import { NextResponse } from 'next/server';
import { CDN_BASE, getSchemaFields, listAll } from '../../../lib/webflow';
import { normalizeItem } from '../../../lib/normalize';
export async function GET() {
const collectionId = process.env.WEBFLOW_COLLECTION_ID;
if (!collectionId) {
return NextResponse.json({ error: 'WEBFLOW_COLLECTION_ID is not set' }, { status: 500 });
}
try {
const [schema, live] = await Promise.all([
getSchemaFields(collectionId),
listAll(CDN_BASE, `/collections/${collectionId}/items/live`),
]);
return NextResponse.json({ items: live.map((item) => normalizeItem(item, schema)) });
} catch (err) {
console.error(err);
return NextResponse.json({ error: 'Upstream request failed' }, { status: 502 });
}
}
The live endpoint returns only published items, so drafts never reach your front end. Calling /api/items locally returns every published item with an identical set of schema keys, and missing values arrive as null.
5. Deploy to Webflow Cloud and run the audit
Commit the four files along with npm's package-lock.json, and deploy the app to the Webflow Cloud environment that holds your variables.
Then rerun the audit against the deployed URL, including the app's mount path, and compare it with the local output to tell whether the problem is in your data or in your environment:
curl -H "x-audit-key: $AUDIT_KEY" https://www.example.com/app/api/fields-audit
Any browser code that calls /api/items has to prefix the mount path itself, because Webflow Cloud does not rewrite client-side fetch calls.
Add a NEXT_PUBLIC_BASE_PATH variable set to your mount path, since Webflow Cloud does not create one, and build the URL as ${process.env.NEXT_PUBLIC_BASE_PATH}/api/items. Once the deployed audit confirms the cause, delete the fields-audit route.
What causes collection fields to go missing from Webflow API responses?
Missing fields usually trace to empty values or keys read by display name. The rest come from the live endpoint's publishing and caching rules, or from fields that hold IDs instead of the content you expected.
A field shows in the CMS panel, but its key is missing from fieldData on some items
Cause: The audit lists that slug as missing on some item IDs and not others, which means those items have no stored value for the field. The API leaves the key out rather than sending an empty value, and its filter docs treat a missing key the same as null.
Fix: Open two of the flagged items in the CMS panel. If the field is blank there, run every item through normalizeItem so blank fields arrive as null, and render a fallback for null in your component.
If the field should never be blank, backfill the empty items and rerun the audit to confirm those IDs drop off the list.
Your code reads the field's display name instead of its slug
Cause: fieldData is keyed by slug, and the schema lists displayName and slug separately. Renaming a field changes its display name but not its slug, so a component reading item.fieldData['Author Name'] or a key it guessed from the label gets undefined on every item.
Fix: Read the schema array from the audit, find the field's display name, and key your component by the slug beside it.
The live endpoint returns fewer fields than the staged endpoint
Cause: /items returns staged items, and /items/live returns only published ones. A slug that appears in live.gaps but not in staged.gaps belongs to edits that have not been published yet.
Fix: Publish the items or the site, then rerun the audit. If the live gaps now match the staged ones, publication was the cause. If the live block holds an error rather than a count, read the status code in the error text first.
Live values lag behind edits you just published
Cause: The production route reads through the Content Delivery API, which caches responses for 300 seconds, or 120 seconds on Enterprise plans. A field you filled in a minute ago can still be missing there while the audit, which reads api.webflow.com, already shows it.
Fix: Check the cf-cache-status response header on a request to the delivery host. If it reads HIT, wait out the cache window before treating the field as missing.
A secondary locale returns blank or different values
Cause: List requests return the primary locale unless you pass a cmsLocaleId, so a secondary locale's translated values never appear in a request without one.
Fix: Get each locale's cmsLocaleId from the Get Site response, then add cmsLocaleId=<id> to the item paths in both routes and rerun the audit for that locale.
A field holds an ID or HTML instead of the value you expected
Cause: The slug is present, but its value is not what the component expected. Per Webflow's field types reference, a Reference field returns one item ID and a Multi-reference field returns an array of IDs. An Option field returns an option ID, and a Rich Text field returns HTML as a single string.
Fix: For references, fetch the related items, build a Map keyed by item ID, and swap each ID with the related item's fields in the route so the browser doesn't request the reference. For options, look the ID up in the field's validations.options from the schema. Render rich text as HTML rather than plain text.
What you can build next with the Webflow API and Webflow Cloud
Once every item arrives with a predictable shape, your collection is reliable source data for anything that reads it, such as a scheduled sync to a database or search index, or a front end built outside the Designer. That shape holds up because the normalizer reads the schema rather than a list of fields you hard-coded.
For the full list of item endpoints and their parameters, Webflow's developer docs cover the CMS API reference.
Frequently asked questions
How many items and collections can a Premium site hold?
The Premium Site plan includes 20,000 CMS items and 40 Collections, according to Webflow's pricing page. At 100 items per request, a collection near that ceiling takes up to 200 list calls, which listAll handles, though the audit then takes a while on a large site.
Can I call the Webflow Data API from page custom code instead of Webflow Cloud?
You can, but a Route Handler on Webflow Cloud keeps the token in a secret environment variable on the server and returns only the normalized fields your page needs. Any token in browser JavaScript is visible to every visitor, and anyone who copies it can read your CMS directly.
Do I need to redeploy after adding a new field to the collection?
No. normalizeItem builds each item from the schema the route fetches on every request, so the new slug appears automatically, as null on items without a value. Redeploy only when your components must render the field, since that change lives in front-end code.





