How to fix Webflow API CORS errors with a Webflow Cloud proxy route

How to fix Webflow API CORS errors with a Webflow Cloud proxy route

Explore how to move your Webflow API token out of page code and into a Webflow Cloud proxy route.

How to fix Webflow API CORS errors with a Webflow Cloud proxy route

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

A Webflow Cloud Route Handler holds your API token on the server and answers your page from your own domain, so the CORS error and the exposed token go away together.

A CORS error on a Webflow API call usually starts with a small script. You add a fetch with an API token to a page's custom code and expect CMS items back. The same request works in curl, but in the browser the Console can print "blocked by CORS policy," and nothing arrives.

CORS is a browser rule, which is why the terminal call succeeds. The bigger problem is the token: anything in the page source is readable by every visitor.

In this guide, we’ll fix both by moving the call into a Next.js Route Handler on Webflow Cloud. It holds the token, calls the API from the server, and returns JSON to your page, with an origin allowlist for pages on another domain.

What do you need to fix Webflow API CORS in Webflow?

Before setting up a server-side proxy route to handle API calls securely, ensure you have the following prerequisites in place:

Here is what you need to get started:

  • Webflow Cloud site: A Webflow site with Webflow Cloud enabled to host the app that serves your page's proxy requests.
  • Next.js 15 or higher: A Next.js app at version 15 or higher, the floor listed on Webflow's bring-your-own-app page.
  • npm: Webflow's docs state, "Currently, Webflow Cloud supports only the npm package manager," so install steps written for pnpm or yarn will not match what the platform builds.
  • A read-only site token: A new token with CMS read access for the site whose content your page reads, replacing the one already shipped in page code.
  • Target endpoint URL: The Data API request your page currently makes, ideally a /items/live URL from the Data API reference so only published items come back.

The route reads the target URL from an environment variable, so the proxy only ever calls that one endpoint. If your page and the proxy route end up on the same origin, the browser skips CORS checks for that call entirely.

5 steps to fix Webflow API CORS in Webflow Cloud

Move the token-bearing request into a Webflow Cloud Route Handler, then point the page at that route. Remove the exposed request first, then build and deploy its replacement.

1. Move the Data API call out of the page's custom code

Find every place a Webflow page sends a request straight to the Data API with a token attached, in page custom code and in embedded code blocks. Open the published page, open your browser's developer tools, and reload with the Network tab visible. A cross-origin request carrying an Authorization header always sends a preflight OPTIONS request first.

If that preflight comes back without an Access-Control-Allow-Origin header matching your page's origin, the browser never sends the real request and logs the CORS error.

The Network tab shows which requests to move.

Write down each request URL, then delete the token and the fetch from the page code and republish. Revoke that token in Site settings and generate a replacement with read-only CMS access, because it has been public for as long as the script was live.

2. Add the token and origin allowlist as environment variables

Store every value the proxy needs as a Webflow Cloud environment variable, and mark the token as secret. Webflow's environment variables docs state that "Both secret and non-secret environment variables are available to your application's build process and to the deployed application at runtime," and secrets are redacted from build logs.

Add these four variables to the environment:

  • WEBFLOW_API_TOKEN: The replacement Data API token, marked secret so it stays out of page code. Only the Route Handler reads it.
  • WEBFLOW_ITEMS_URL: The exact request URL from step 1, fixed server-side so visitors cannot reach other API paths. Use a /items/live URL so the route only ever returns published items, because anyone can call a public route with curl, and a staged /items URL would leak drafts.
  • ALLOWED_ORIGINS: A comma-separated list of full origins allowed to call the route, such as a staging domain; leave empty for same-domain callers.
  • NEXT_PUBLIC_BASE_PATH: The app's mount path, for example /app, for browser code inside the app. Webflow Cloud does not create this variable, so add it yourself.

For local work, put the same four keys in .env.local, which Next.js loads under next dev.

3. Write the proxy Route Handler

Create app/api/items/route.ts. The handler answers preflight requests, reads the token at request time, calls the single upstream URL, and passes the status and body back. Export only GET and OPTIONS; nothing from the request reaches the upstream URL, and no credentials are allowed, so the route cannot be turned into an open proxy.

The origin allowlist is not authentication, because a script outside a browser can send any Origin it likes.

Keep the response limited to content you would publish anyway, and add authentication before exposing anything private:

// app/api/items/route.ts
// Public, read-only proxy. The origin allowlist is not authentication:
// only return content you would publish anyway.
import { NextRequest, NextResponse } from 'next/server';

function corsHeaders(origin: string | null): Record<string, string> {
  const allowed = (process.env.ALLOWED_ORIGINS ?? '')
    .split(',')
    .map((value) => value.trim())
    .filter(Boolean);

  if (origin && allowed.includes(origin)) {
    return {
      'Access-Control-Allow-Origin': origin,
      'Access-Control-Allow-Methods': 'GET, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
      Vary: 'Origin',
    };
  }

  return { Vary: 'Origin' };
}

export async function OPTIONS(request: NextRequest) {
  return new NextResponse(null, {
    status: 204,
    headers: corsHeaders(request.headers.get('origin')),
  });
}

export async function GET(request: NextRequest) {
  const headers = corsHeaders(request.headers.get('origin'));
  const token = process.env.WEBFLOW_API_TOKEN;
  const itemsUrl = process.env.WEBFLOW_ITEMS_URL;

  if (!token || !itemsUrl) {
    return NextResponse.json(
      { error: 'Server is missing WEBFLOW_API_TOKEN or WEBFLOW_ITEMS_URL' },
      { status: 500, headers },
    );
  }

  const upstream = await fetch(itemsUrl, {
    headers: {
      Authorization: `Bearer ${token}`,
      accept: 'application/json',
    },
  });

  const body = await upstream.text();

  return new NextResponse(body, {
    status: upstream.status,
    headers: { ...headers, 'Content-Type': 'application/json' },
  });
}

The route reflects an origin only when it appears in ALLOWED_ORIGINS. I prefer that to allowing every origin with a wildcard. Run npm run dev and request /api/items in your browser; you should see the Data API's JSON response, served from your own app.

4. Call the route from the app with the base path

Include the mount path in every client-side request. The bring-your-own-app page states it directly: "Client-side fetch calls must manually include the base path." Webflow Cloud sets basePath at build time and overwrites any value you commit, so leave it out of next.config and read NEXT_PUBLIC_BASE_PATH from step 2 instead.

A client component that loads the proxied items looks like this:

// app/components/ItemsPreview.tsx
'use client';

import { useEffect, useState } from 'react';

const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';

export default function ItemsPreview() {
  const [output, setOutput] = useState('Loading…');

  useEffect(() => {
    fetch(`${basePath}/api/items`)
      .then(async (res) => {
        const text = await res.text();
        setOutput(res.ok ? text : `Error ${res.status}: ${text}`);
      })
      .catch((err) => setOutput(`Request failed: ${String(err)}`));
  }, []);

  return <pre>{output}</pre>;
}

The component builds the URL from the injected prefix, so the same code works under next dev (where the variable can be empty) and on the mounted deployment. Render it on any page in your app. Locally, the pre block should fill with the same JSON you saw in step 3.

5. Point the Webflow page at the proxy and deploy

Replace the script you removed in step 1 with one that calls your route. Compare the page URL with the request URL in the Network tab. If the scheme, host and port match, the browser treats the call as same-origin. If they differ, add that page's full origin to ALLOWED_ORIGINS instead.

Change /app to your mount path. For a cross-origin call, replace /app/api/items with the proxy's full URL. Add an element with a data-items-output attribute to hold the result.

Add this script to the page's custom code:

<script>
  document.addEventListener('DOMContentLoaded', async () => {
    const target = document.querySelector('[data-items-output]');
    if (!target) return;

    try {
      const res = await fetch('/app/api/items');
      if (!res.ok) throw new Error('Proxy returned ' + res.status);
      target.textContent = JSON.stringify(await res.json(), null, 2);
    } catch (err) {
      target.textContent = 'Could not load items: ' + err.message;
    }
  });
</script>

Deploy the app to its Webflow Cloud environment, then publish the site. Reload the page with the Network tab open: you should see one request to the proxy's domain and mount path, such as /app/api/items, returning 200, and no CORS error in the Console.

What causes a Webflow API CORS fix to fail?

Most failures come from a leftover browser request or an origin missing from the allowlist; the rest come from the mount path or deployment variables. For build errors from an edge runtime export, the webhooks and REST API guide covers the fix in detail.

The Console still shows "blocked by CORS policy" after the proxy is live

Cause: The failing request is either still going to the Data API from an old script, or it reaches your route from an origin missing from ALLOWED_ORIGINS. A page on the webflow.io staging domain that calls a route on your custom domain is the usual case, because it sends the staging origin in its Origin header.

A second cause shows up on Next 16 projects: CORS logic placed in proxy.ts never runs, because Webflow's framework customization docs state, "Node.js runtime middleware isn't supported. Only Edge runtime middleware works on the Workers runtime," and proxy runs on Node.

Fix: Check the request URL in the Network tab first. If it points to the Data API, find the leftover script and remove it. If it points at your route, copy the exact Origin request header, including scheme and any port, into ALLOWED_ORIGINS and redeploy so the handler reads the new value. Keep CORS headers in the Route Handler or in middleware.ts, never in proxy.ts.

A direct call to a live items URL seems to work without the proxy

Cause: Some Data API endpoints, including the live items endpoints, answer a browser's preflight with CORS headers, so a page script that calls them directly with a token can succeed. The CORS error disappears, but the token is still sitting in the page source for every visitor to copy.

Fix: Treat the missing error as luck, not a fix. Move the call to the proxy route anyway, revoke the token that was in the page, and keep the replacement only in the Webflow Cloud environment.

The proxy returns 404 in production but works under next dev

Cause: Browser fetch calls do not automatically pick up the mount path Webflow Cloud injects at build time. A request without that prefix can work under next dev but return 404 on the mounted deployment.

Fix: Check the failing request for the mount-path prefix. Confirm NEXT_PUBLIC_BASE_PATH is set in the deploy environment and matches the mount path exactly, leading slash included. Redeploy after changing it.

For scripts in Webflow page custom code, hardcode the mount path, since those scripts sit outside the Next.js build entirely. Compare the request URL in the Network tab with the app's mount path, not just the route's /api/items path.

The route returns 401 or 500 after deployment but works locally

Cause: The deployed environment is missing WEBFLOW_API_TOKEN or WEBFLOW_ITEMS_URL. A successful local run can mask those missing deployment variables. A 500 with the route's own "Server is missing" message means a variable is absent. A 401 passed through from the Data API points at the token itself: check that the deployed value is your replacement token and that it can read the content the request asks for.

Fix: Confirm both keys exist in the exact Webflow Cloud environment you deployed. If the token is the one you replaced in step 1, swap in the new value. Redeploy after any change, then reload the published page and inspect the response in the Network tab.

What you can build next with the Webflow API and Webflow Cloud

Custom listings and filtered views can use the same route, as can app pages that read your Webflow CMS content. Each new read your app needs gets its own fixed route, so the proxy grows into a small content layer without ever exposing the token.

If a proxied route starts returning 429s under traffic, the guide to fixing rate limits covers caching on the server.

For deeper customization beyond a single proxy route, Webflow's developer docs list the Webflow Cloud limits every route runs within.

Frequently asked questions

Can I use a public CORS proxy service instead of a Route Handler?

Yes, you can use a public CORS proxy to make the error disappear. I recommend a Webflow Cloud Route Handler instead. Sending the token from page code through a public proxy also exposes it to a third party's server. Anyone who reads it can call the Data API as you.

Do requests to the proxy route count against Webflow Cloud limits?

Yes. Each request runs your app's code, so it counts toward your Webflow Cloud request allowance, while the route's call to Webflow is a subrequest that doesn't. A Cache-Control header on the route won't reduce the load because Webflow Cloud replaces it with private, no-cache; pointing WEBFLOW_ITEMS_URL at the Content Delivery API host, api-cdn.webflow.com, serves published items from a cache for up to 300 seconds instead.


Last Updated
October 30, 2026
Category

Related articles


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo

Jetzt kostenlos loslegen

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Get started — it’s free
Demo ansehen
Demo ansehen

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.