IntegrationsSTATIC SITES

Connect a static website to live APIs without a backend

Static websites often need live records, weather figures, or catalog metrics from public HTTP endpoints. When an upstream service omits cross-origin permissions, browser security blocks direct access. You can handle these requests reliably by checking headers, rendering explicit states, and using a proxy only when required.

Inspect upstream CORS headers before adding infrastructure

Many public data providers already send an Access-Control-Allow-Origin header on their responses. Before writing proxy logic or changing endpoints, test the upstream endpoint directly. A fast terminal check shows whether the browser can consume the response directly.

Check the response headers with curl
$ curl -i https://api.open-meteo.com/v1/forecast?latitude=51.92&longitude=4.48&current=temperature_2m

HTTP/2 200
content-type: application/json; charset=utf-8
access-control-allow-origin: *
# ^ browsers may read this response from any origin

When the response contains an Access-Control-Allow-Origin header matching * or your origin, proceed straight to the widget. If the header is missing and the target holds public data, route the request through a proxy. You can verify extracted headers with the header checker.

Render explicit loading, error, and success states

A production widget must handle all network phases cleanly in plain HTML and JavaScript. Guard against malformed JSON by validating response.ok before reading the body. Render a clear loading indicator, display a timestamp on success, and inform the visitor if the network request fails.

A complete weather widget, no framework
<div id="weather">Loading the weather…</div>

<script type="module">
  const box = document.getElementById('weather');
  const url =
    'https://api.open-meteo.com/v1/forecast?latitude=51.92&longitude=4.48&current=temperature_2m';

  async function load() {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`Request failed: ${response.status}`);
      const data = await response.json();
      const time = new Date().toLocaleTimeString();
      box.textContent = `It is ${data.current.temperature_2m}°C in Rotterdam (updated ${time}).`;
    } catch (error) {
      box.textContent = `Could not load the weather: ${error.message}`;
    }
  }

  void load();
  // Weather does not change every second — refresh every 15 minutes
  const timer = setInterval(load, 15 * 60 * 1000);
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) clearInterval(timer);
  });
</script>

The catch block ensures errors surface directly inside the container instead of failing silently in the developer console. Recording a timestamp lets visitors distinguish fresh metrics from stale cached elements. This structure works without bundlers, libraries, or external runtimes.

Select a deliberate refresh interval

Decide how often the widget fetches fresh data. A manual button gives visitors control without consuming network traffic in background tabs. If automatic polling fits the data update frequency, register a listener on document.addEventListener('visibilitychange') to halt intervals when the tab loses focus. The anonymous tier applies fair-use rate limits per IP, and every interval creates a recurring commitment across active readers.

Route through the proxy when CORS headers are missing

When an upstream provider returns no CORS headers and the data is public, prefix the target URL with https://proxy.cors.dev/. Browsers send the Origin header automatically during cross-origin fetch operations, which the proxy inspects. The proxy operator can observe traffic routed through the service, which means it remains suitable for public data only.

Only the URL changes
// Step 1 showed this API sends no CORS headers. The data is public, so:
const url = 'https://proxy.cors.dev/https://api.example.com/data';

// Everything else about the widget stays the same
const response = await fetch(url);

The service limits responses to 1 MiB and upstream timeouts to 10 seconds. GET and HEAD requests are permitted, while requests carrying cookies or authorization tokens are rejected outright. To monitor requests or bind calls to registered production origins, create a free account to receive an X-Cors-Key publishable key. You can also test requests interactively in the playground.

Verify the live deployment on its final origin

Browsers enforce CORS policies strictly per origin, meaning requests from localhost do not prove that a production deployment will succeed. Deploy the static files to your production host, open the Network panel in developer tools, and confirm the outgoing request origin matches the expected domain. Simulate an outage by enabling request blocking for the target URL to ensure the interface renders the failure state predictably.

Good questions.

Can I call an API that needs an API key from a static site?

Browser code reveals every embedded string, secret, and header to anyone opening browser developer tools. Private provider credentials belong on your own server or a serverless function, not in static client files. The publishable key from cors.dev is built for static source code: it is an identifier rather than a secret, origin checks stop other websites from reusing it in browser code, and a misused key can be rotated or revoked in the dashboard.

What about RSS or CSV feeds?

Static sites can fetch raw text formats such as RSS feeds, XML, or CSV tables. The browser parses these payloads via native APIs like DOMParser once response.ok confirms a successful transfer. If you route non-JSON feeds through the proxy, verify that upstream responses stay below the 1 MiB limit and use GET or HEAD methods.

How do I keep the widget from hammering the API?

Align client polling with the actual cadence of upstream updates instead of setting aggressive intervals. Pause recurring timer loops whenever document.hidden becomes true so background browser tabs do not consume quota. For readouts that change infrequently, replace automated polling with a manual refresh button.