GuidesWEBFLOW

Fix CORS errors in Webflow custom code

A fetch() inside a Webflow Embed element or page-level custom code fails with a CORS error when the API it calls sends no Access-Control-Allow-Origin header, because the request runs in the visitor browser from your site origin (yoursite.webflow.io or your custom domain) and Webflow hosting has no server route to send it through. For public GET requests, put https://proxy.cors.dev/ in front of the API URL: free, no signup, no API key, JSON, XML and HTML responses up to 1 MiB within 10 seconds. APIs that need a secret key belong behind a backend: Webflow Cloud, a serverless function, or an automation that writes into the CMS.

Where Webflow custom code runs

Everything you paste into an Embed element, the page settings head and footer, or the site-wide custom code runs in the browser of each visitor after the page loads; the Designer shows a placeholder and runs none of it. The browser applies the same-origin policy to every fetch() or XMLHttpRequest your script makes: a response from another origin is handed to your code only when it carries Access-Control-Allow-Origin with your site origin or *. Your staging domain and your custom domain are two different origins, so an API that allows one by name does not allow the other.

Chrome reports Access to fetch at ... from origin https://yoursite.webflow.io has been blocked by CORS policy and the fetch rejects with TypeError: Failed to fetch. The request usually reached the API; the browser withheld the answer. One check tells you whether a given API needs help.

Check the API before touching your Webflow project
# No output: the API sends no CORS header, so the embed will be blocked
curl -sI -H 'Origin: https://yoursite.webflow.io' https://feeds.bbci.co.uk/news/technology/rss.xml \
  | grep -i access-control-allow-origin

# access-control-allow-origin: * means fetch() works directly, no proxy needed
curl -sI -H 'Origin: https://yoursite.webflow.io' https://api.frankfurter.app/latest?from=EUR \
  | grep -i access-control-allow-origin

APIs that work directly

APIs built for browser use send Access-Control-Allow-Origin: * and need nothing from you: api.github.com, api.open-meteo.com, api.frankfurter.app, most headless CMS delivery APIs and payment widgets do. Call those as-is. The embed below is for the other kind: feeds, scores, prices and legacy JSON endpoints that answer without the header.

Public data: prefix a proxy in the embed

A CORS proxy fetches the API from its own servers and relays the response with Access-Control-Allow-Origin set to the calling origin, so the browser lets your script read it. With cors.dev that is the URL prefix and nothing else. The example shows the five latest headlines of a feed that sends no CORS header; swap in any public feed or JSON API. Build the DOM with textContent and elements rather than innerHTML, since the data comes from another site.

Embed element: live headlines from a feed without CORS headers
<ul id="headlines"><li>Loading headlines...</li></ul>

<script>
  (async () => {
    const list = document.getElementById('headlines');
    const feed = 'https://feeds.bbci.co.uk/news/technology/rss.xml';

    try {
      const response = await fetch('https://proxy.cors.dev/' + feed, { credentials: 'omit' });
      if (!response.ok) {
        throw new Error(response.headers.get('X-Cors-Error') || 'HTTP ' + response.status);
      }

      const xml = new DOMParser().parseFromString(await response.text(), 'application/xml');
      const items = [...xml.querySelectorAll('item')].slice(0, 5);

      list.replaceChildren(
        ...items.map((item) => {
          const li = document.createElement('li');
          const link = document.createElement('a');
          link.href = item.querySelector('link')?.textContent.trim() || '#';
          link.textContent = item.querySelector('title')?.textContent.trim() || 'Untitled';
          link.target = '_blank';
          link.rel = 'noopener';
          li.append(link);
          return li;
        }),
      );
    } catch (error) {
      list.textContent = 'Could not load headlines: ' + error.message;
    }
  })();
</script>

Publish the site to see it run: embeds do not execute inside the Designer. Anonymous requests work from the webflow.io staging domain and from custom domains alike, cover GET and HEAD to any public HTTPS host, forward only Accept, If-None-Match and If-Modified-Since, and never forward cookies. Responses keep the upstream status and Content-Type; a proxy-side failure sets X-Cors-Error, for example response_too_large above 1 MiB or upstream_timeout after 10 seconds.

APIs with keys: not in an embed

Anything in custom code is visible to every visitor through view-source, including API keys, and the anonymous proxy rejects credential-like query names such as key and custom headers such as X-Api-Key for that reason. Keyed integrations belong on a server: a Webflow Cloud route on your own origin, a small serverless function that holds the key and answers with CORS headers, or an automation (Make, Zapier, n8n) that writes the data into CMS items on a schedule so the page needs no runtime call at all. A Pro Connection forwards Authorization and custom headers, which fits per-visitor tokens, not a shared secret.

Limits and pricing

cors.dev limits for a Webflow site
LimitFree, no keyPro, $5 per month
Price$0$5 per month; 7-day trial with 1,000 requests, no card
MethodsGET and HEADGET, HEAD, POST, PUT, PATCH and DELETE
DestinationsAny public HTTPS hostPublic HTTPS hosts enabled on your Connection
Response size1 MiB, text types only6 MiB, any content type
Request bodyNone1 MiB
Upstream deadline10 seconds10 seconds
RateShared pool with fair-use limits600 requests per minute, 10 concurrent per account
Monthly requestsNo quota500,000 per billing period, $5 per extra 500,000
Request headers forwardedAccept, If-None-Match, If-Modified-SincePlus Authorization and custom headers
Page originsAny, staging and custom domainsOrigins listed on your Connection
CachingNone, no-storeOpt-in X-Cors-Cache, 1 to 300 seconds

Good questions.

Why is my embed empty in the Designer?

Scripts in Embed elements and custom code only run on the published site. Publish to staging, open the webflow.io URL and check the browser console there.

Does it work on the webflow.io staging domain and the custom domain?

Yes. Anonymous proxy requests do not depend on the calling origin, so the same embed works on staging, on the live domain and when you open the exported HTML locally.

Can I hide an API key in Webflow custom code?

No. Custom code is downloaded by every visitor. Put keyed calls on a Webflow Cloud route, a serverless function, or an automation that writes results into the CMS, and let the page read from there.

Can I POST a form to an external API from Webflow?

Anonymous requests are GET and HEAD only. A Pro Connection forwards POST, PUT, PATCH and DELETE with bodies up to 1 MiB to the hosts you enable; for lead forms, native Webflow forms with a webhook or an automation are usually simpler.

How often can the embed call the API?

Anonymous traffic shares a fair-use pool and waits up to 2 seconds for a slot before a 429 with Retry-After. A headline list that loads once per page view fits comfortably; for heavy traffic, Pro gives 600 requests per minute, 10 concurrent requests and opt-in caching from 1 to 300 seconds.

What if the API answers with an error?

The status passes through unchanged with X-Cors-Source: upstream, so a 404 or 500 from the API shows as that. When the proxy itself could not complete the request you get X-Cors-Error, and X-Cors-Request-Id identifies the request if you need support.