GuidesCHATGPT SITES

ChatGPT Sites and CORS errors

A ChatGPT Site runs its page code in the visitor's browser, so a fetch() to an API that sends no Access-Control-Allow-Origin header fails with TypeError: Failed to fetch. Put https://proxy.cors.dev/ in front of the API URL to fix it: the free tier needs no signup or key, allows about 600 requests a minute per visitor and returns responses up to 6 MiB. Of 13 APIs that real Sites call, tested from a chatgpt.site origin on 6 November 2026, the 7 without CORS headers answered 200 through the proxy and 4 already work directly.

Why fetch() fails on a ChatGPT Site

ChatGPT Sites builds, hosts and shares a website from a prompt in ChatGPT or Codex, and it has been in public beta since 9 July 2026. A published Site loads from its own chatgpt.site subdomain, such as https://stock-pulse.example.chatgpt.site, or from a custom domain you connect.

The page code runs in the visitor's browser. A fetch() from that page to another domain, such as query1.finance.yahoo.com, is a cross-origin request, so the browser only hands the response to your script when the API answers with an Access-Control-Allow-Origin header that allows the Site's origin. Yahoo Finance and every RSS feed we tested send no such header, and Chrome logs this:

Chrome console on a ChatGPT Site
Access to fetch at 'https://query1.finance.yahoo.com/v8/finance/chart/AAPL' from origin 'https://stock-pulse.example.chatgpt.site' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
TypeError: Failed to fetch

Setting mode: 'no-cors', as the message suggests, does not help: the browser returns an opaque response with status 0 and an empty body. None of the 19 public Sites we checked on 6 November 2026 sends a Content-Security-Policy header, so the request does leave the page; the missing CORS header on the API is what blocks the response.

Fix it with the proxy.cors.dev prefix

Put https://proxy.cors.dev/ in front of the full API URL, so https://query1.finance.yahoo.com/v8/finance/chart/AAPL?range=1mo&interval=1d becomes https://proxy.cors.dev/https://query1.finance.yahoo.com/v8/finance/chart/AAPL?range=1mo&interval=1d. The proxy fetches the API from a server, where CORS does not apply, and answers your page with Access-Control-Allow-Origin set to the Site's origin.

  • No signup and no key: GET and HEAD requests work right away, from any origin.
  • About 600 requests a minute per IP. Each visitor's browser sends its own requests, so the limit applies per visitor, with no daily or monthly quota.
  • Responses up to 6 MiB, and the proxy waits up to 30 seconds for the API to answer.
  • Your script can read X-Cors-Error and Retry-After on every response. Cookies are never forwarded.
proxy.js: load JSON and RSS through proxy.cors.dev from a Site's page
// Use this for APIs that send no Access-Control-Allow-Origin header
const PROXY = 'https://proxy.cors.dev/';

async function fetchThroughProxy(url) {
  const response = await fetch(PROXY + url, { signal: AbortSignal.timeout(30_000) });

  if (!response.ok) {
    const reason = response.headers.get('X-Cors-Error') ?? `HTTP ${response.status}`;
    throw new Error(`${new URL(url).hostname}: ${reason}`);
  }
  return response;
}

const chart = await fetchThroughProxy(
  'https://query1.finance.yahoo.com/v8/finance/chart/AAPL?range=1mo&interval=1d',
).then((response) => response.json());

const { meta } = chart.chart.result[0];
console.log(meta.symbol, meta.regularMarketPrice, meta.currency);

const feed = await fetchThroughProxy('https://feeds.bbci.co.uk/news/business/rss.xml').then((response) =>
  response.text(),
);
const xml = new DOMParser().parseFromString(feed, 'application/xml');
console.log([...xml.querySelectorAll('item > title')].slice(0, 3).map((title) => title.textContent));

ChatGPT already writes this pattern on its own. Between 2 October and 6 November 2026, 72 ChatGPT Sites sent 239,328 requests through proxy.cors.dev and 86% came back 200. 48 of them load Yahoo Finance, which is 87% of that traffic, and the public ones we read mostly build the call as 'https://proxy.cors.dev/' + url.

Ask ChatGPT to make the change

You do not have to edit the code by hand. Ask ChatGPT for the change in the Site editor:

  1. Open sites

    On the web, select More, then sites, or go to chatgpt.com/sites, and open the Site.

  2. Select Edit

    In the preview, select Edit. Under Describe website edits, paste the requests from the list below.

  3. Review before it goes live

    Every Sites deployment URL is a production deployment. Ask ChatGPT to save a version without deploying it, check that the data loads, then ask it to deploy that version.

Ask for these changes:

  • Send requests to APIs that return no CORS headers to https://proxy.cors.dev/ followed by the full API URL.
  • Keep direct calls to APIs that already send Access-Control-Allow-Origin.
  • Allow up to 30 seconds per request.
  • After a 429, wait the number of seconds in Retry-After before trying again, and never retry in a loop.
  • Load Yahoo Finance quotes 20 symbols per request with /v7/finance/spark.
  • Send no Authorization or API key headers through the proxy.

APIs ChatGPT Sites call, tested

On 6 November 2026 we called 13 APIs that real Sites use, with the origin https://stock-pulse.example.chatgpt.site, once through proxy.cors.dev and once directly. 7 send no CORS header and answered 200 through the prefix, 4 already send CORS headers and work directly, and 2 fail either way.

APIs called from a chatgpt.site origin, checked November 2026
APIThrough proxy.cors.devDirect from the SiteWhat to do
Yahoo Finance chart200Blocked, no CORS headerUse the prefix
Yahoo Finance spark200Blocked, no CORS headerUse the prefix
Yahoo Finance headlines RSS200403, no CORS headerUse the prefix
BBC News RSS200Blocked, no CORS headerUse the prefix
Federal Reserve press RSS200Blocked, no CORS headerUse the prefix
EIA Today in Energy RSS200Blocked, no CORS headerUse the prefix
NOS news RSS200Blocked, no CORS headerUse the prefix
SEC EDGAR submissions200, not neededWorks, sends *Call it directly
Eastmoney quotes200, not neededWorks, echoes the originCall it directly
Radio Browser200, not neededWorks, sends *Call it directly
CoinGecko simple price429 from CoinGeckoWorks, sends *Call it directly
Yahoo Finance quote401, needs a Yahoo cookie401, no CORS headerUse chart or spark
Google News RSS503 from GoogleBlocked, no CORS headerUse another feed

CoinGecko answered 429 through the proxy but works directly, so call it from the page like the other APIs that send CORS headers. Yahoo's /v7/finance/quote needs a Yahoo cookie and crumb, which cors.dev never forwards, so use /v8/finance/chart/<symbol> or /v7/finance/spark instead. Google answers the proxy's servers with a 503 most of the time: 68% of the Google News requests Sites sent through the proxy since 2 October 2026 got one, so load a feed such as BBC News RSS.

Stay under the rate limits

Pick the Yahoo Finance endpoint by how many tickers you load. /v8/finance/chart/<symbol> returns one symbol with its price, previous close and history. /v7/finance/spark?symbols=AAPL,MSFT,NVDA returns up to 20 symbols per request, and 21 symbols get a 400, so a 300-ticker watchlist is 15 spark requests instead of 300 chart requests.

Since 2 October 2026, all 11,107 requests cors.dev answered with 429 rate_limited came from one Site whose page went past 600 requests a minute, and the same Site drew 6,016 of the 6,347 429 answers Yahoo sent to Sites. Tell the two apart with X-Cors-Source: gateway means cors.dev's limit, with Retry-After: 60, and upstream means the API answered 429 itself.

Pages also cancelled 1,907 requests before the answer arrived. Generated code sets its own timeouts, and the public Sites we read used 2.5, 12 and 18 seconds, while the proxy gives a slow API up to 30 seconds.

watchlist.js: 20 Yahoo symbols per request, waiting out a 429
const PROXY = 'https://proxy.cors.dev/';

// After a 429, wait as long as Retry-After asks, up to 3 tries
async function getJson(url, tries = 3) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(PROXY + url, { signal: AbortSignal.timeout(30_000) });
    if (response.ok) return response.json();

    if (response.status !== 429 || attempt === tries) {
      throw new Error(response.headers.get('X-Cors-Error') ?? `HTTP ${response.status}`);
    }
    const seconds = Number(response.headers.get('Retry-After')) || 10;
    await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
  }
}

// Yahoo's spark endpoint answers for up to 20 symbols per request
async function loadWatchlist(symbols) {
  const quotes = {};
  for (let start = 0; start < symbols.length; start += 20) {
    const batch = symbols.slice(start, start + 20).map(encodeURIComponent).join(',');
    const body = await getJson(
      `https://query1.finance.yahoo.com/v7/finance/spark?symbols=${batch}&range=1d&interval=1d`,
    );
    for (const { symbol, response } of body.spark.result) {
      const { regularMarketPrice: price, chartPreviousClose: previous, currency } = response[0].meta;
      quotes[symbol] = { price, change: Number((price - previous).toFixed(2)), currency };
    }
  }
  return quotes;
}

console.table(await loadWatchlist(['AAPL', 'MSFT', 'NVDA', 'ASML.AS', '^GSPC', 'BTC-USD']));

API keys, POST and server code

The free tier refuses Authorization and API key headers such as X-Api-Key with 400 header_not_allowed. Other headers it does not forward are dropped and listed in X-Cors-Dropped-Headers. A key in the query string is forwarded as it is, and every visitor can read it in the page.

A secret key belongs on the Site's server, not in the page. Store it under More actions, then Settings, where Sites keeps hosted environment variables and secrets, and ask ChatGPT to call that API from server-side code. A proxy cannot hide a key: anything the page sends, its visitors can read.

For POST, PUT, PATCH or DELETE, request bodies up to 1 MiB, or Authorization and custom headers from the page, use cors.dev Pro: $5 a month for 500,000 requests, with a 7-day trial of 1,000 requests and no card. Sign up with Google, create a Connection with the Site's exact chatgpt.site origin, any custom domain and the API hosts, and send its publishable key in X-Cors-Key. Throughput is 3,000 requests a minute per account and 100 in flight, and it suits APIs where each visitor enters their own key.

Good questions.

Does proxy.cors.dev work with a custom domain on a ChatGPT Site?

Yes. The free tier accepts any origin, so the Site works the same on its chatgpt.site address and on a connected custom domain. With cors.dev Pro, add both origins to the Connection. OpenAI's docs say custom domains are not available in Enterprise workspaces at launch.

Does it work on a workspace-only Site or one with Sign in with ChatGPT?

Yes. A Site restricted to a workspace, or one that offers Sign in with ChatGPT, still runs its page code in the visitor's browser and sends its own origin. cors.dev needs no ChatGPT sign-in: 51 of the 72 Sites that called the proxy by 6 November 2026 sit behind a ChatGPT login.

Do requests to proxy.cors.dev count toward ChatGPT Sites hosting usage?

Sites hosting is free through 31 December 2026. From 1 January 2027 OpenAI plans to meter usage above each plan's included monthly allowance, with traffic measured as the requests your Site handles and proposed at about 16 credits per 100,000. A request from the visitor's browser to proxy.cors.dev goes straight to cors.dev and never reaches your Site.

Why does my Site get 429 Too Many Requests through the proxy?

Either cors.dev or the API is limiting it. X-Cors-Source: gateway with X-Cors-Error: rate_limited means one visitor went past about 600 requests a minute; wait the 60 seconds in Retry-After. X-Cors-Source: upstream means the API itself answered 429, as Yahoo Finance and CoinGecko do, so send fewer requests or batch them.

Why do requests show as cancelled in the browser's network tab?

The Site's own code aborted them. Generated code can add a timeout with AbortController or AbortSignal.timeout(), and a short one cancels the request while the proxy is still waiting on a slow API, which it allows up to 30 seconds. Ask ChatGPT to raise the timeout to 30 seconds.