GuidesAPI PROXY
API proxy for frontend apps
An API proxy for a frontend app fetches a third-party API server-side and returns the response with the CORS headers the browser requires. On cors.dev that is one URL prefix: https://proxy.cors.dev/ followed by the API URL. GET and HEAD requests to any public HTTPS API are free with no signup and no API key, for JSON, XML, HTML and other text responses up to 1 MiB within 10 seconds. Pro costs $5 per month and adds POST, PUT, PATCH and DELETE, Authorization and custom headers, 6 MiB responses and 500,000 requests per billing period.
What an API proxy does
Browsers enforce the same-origin policy: page code on your origin can read a response from another origin only if that response carries an Access-Control-Allow-Origin header naming your origin or *. Most public APIs were built for servers, not browsers, and send no such header. An API proxy sits in between and does four things.
- Takes the request from your page
The browser sends the request to
proxy.cors.devwith your pageOrigin. PreflightOPTIONSrequests are answered by the proxy itself, withAccess-Control-Max-Age: 600, so the API never sees them. - Fetches the API from its own servers
No browser policy applies server-side. Cookies are never forwarded; anonymously only
Accept,If-None-MatchandIf-Modified-Sincetravel upstream, so your request cannot leak browser state. - Relays the answer with the right headers
Status code,
Content-Typeand body come back unchanged, withAccess-Control-Allow-Originset to your exact origin.ETag,Last-Modified,Link,Retry-Afterand theX-RateLimit-*headers are exposed to your code. - Tells you who failed
X-Cors-Source: upstreammeans the API answered.X-Cors-Source: gatewayplus anX-Cors-Errorcode means the proxy refused or could not complete the request, andX-Cors-Request-Ididentifies it in support.
When a frontend needs one, and when it does not
You need a proxy when the API is not yours, sends no CORS headers, and your app has no server of its own: a static site on GitHub Pages or S3, a Chrome extension or Figma plugin, a prototype, a widget embedded in a no-code builder. You can test any URL in 10 seconds: curl -sI -H "Origin: https://example.com" <api-url> | grep -i access-control-allow-origin. No output means the browser will block a direct call.
You do not need one when the API already sends Access-Control-Allow-Origin: *: api.github.com, api.open-meteo.com and most APIs designed for browser use do, and routing them through a proxy only adds a hop and shares one IP address between all your visitors, which is how GitHub API rate limit exceeded happens. When the API is yours, add the headers on your server instead. When the request carries a secret, read managed proxy or your own backend first.
The free tier: one prefix, no account
Anonymous requests cover GET and HEAD to any public HTTPS host on port 443. The response body must be a text type: JSON, XML, HTML, CSV, plain text, JavaScript or SVG, up to 1 MiB, delivered within a 10 second upstream deadline. Redirects are followed for up to 4 hops as long as every hop stays on public HTTPS. Responses are passed through with Cache-Control: no-store, and there are no upstream retries, so one request on your side is one request on the API.
// Fails in the browser: store.steampowered.com sends no Access-Control-Allow-Origin
const direct = await fetch('https://store.steampowered.com/api/appdetails?appids=440');
// Works: the proxy fetches it server-side and answers with the CORS headers your origin needs
const response = await fetch(
'https://proxy.cors.dev/https://store.steampowered.com/api/appdetails?appids=440',
{ credentials: 'omit' },
);
const data = await response.json();
console.log(data['440'].data.name); // "Team Fortress 2"Errors keep the upstream status when the API answered, and use a proxy status with an X-Cors-Error code when it did not. Anonymous traffic shares one fair-use pool: a busy pool queues a request for up to 2 seconds before answering 429 with Retry-After. Respect that header rather than retrying in a loop.
const response = await fetch('https://proxy.cors.dev/' + apiUrl, { credentials: 'omit' });
if (!response.ok) {
// Set by the proxy itself, for example response_too_large or upstream_timeout.
// Absent when the upstream API answered with an error status of its own.
const code = response.headers.get('X-Cors-Error');
const requestId = response.headers.get('X-Cors-Request-Id');
const retryAfter = response.headers.get('Retry-After');
if (response.status === 429 && retryAfter) {
await new Promise((resolve) => setTimeout(resolve, Number(retryAfter) * 1000));
return fetchAgain();
}
throw new Error(`HTTP ${response.status}: ${code ?? 'upstream error'} (${requestId})`);
}Pro: write methods, Authorization and bigger responses
A Connection is a saved project config: the page origins that may call it, the API hosts it may reach, and one or more publishable keys sent as X-Cors-Key. With a trial or Pro Connection the proxy forwards POST, PUT, PATCH and DELETE with request bodies up to 1 MiB, forwards an explicit Authorization header and custom API headers, relays any content type up to 6 MiB, and accepts credential-like query names such as key that anonymous requests reject. Opt-in caching per request with X-Cors-Cache: 60 (1 to 300 seconds) spares the API on hot endpoints.
// The Connection allows your origin, the API host, and this publishable key.
// Authorization is forwarded to the API; the proxy never stores it.
const response = await fetch('https://proxy.cors.dev/https://api.example.com/items', {
method: 'POST',
headers: {
'X-Cors-Key': 'YOUR_CONNECTION_KEY',
Authorization: `Bearer ${userToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'New item' }),
credentials: 'omit',
});Pro is $5 per month for 500,000 requests per monthly billing period, at 600 requests per minute and 10 concurrent requests per account, with $5 prepaid blocks of 500,000 extra requests. The trial runs 7 days with 1,000 requests total at 120 per minute and 5 concurrent, and needs no card. Every admitted request counts, including cache hits and upstream failures.
Limits and pricing
| Limit | Free, no key | Pro, $5 per month |
|---|---|---|
| Price | $0 | $5 per month; 7-day trial with 1,000 requests, no card |
| Methods | GET and HEAD | GET, HEAD, POST, PUT, PATCH and DELETE |
| Destinations | Any public HTTPS host | Public HTTPS hosts enabled on your Connection |
| Response size | 1 MiB, text types only | 6 MiB, any content type |
| Request body | None | 1 MiB |
| Upstream deadline | 10 seconds | 10 seconds |
| Rate | Shared pool with fair-use limits | 600 requests per minute, 10 concurrent per account |
| Monthly requests | No quota | 500,000 per billing period, $5 per extra 500,000 |
| Request headers forwarded | Accept, If-None-Match, If-Modified-Since | Plus Authorization and custom headers |
| Redirects | Followed, up to 4 hops | Followed when every hop host is enabled |
| Caching | None, no-store | Opt-in X-Cors-Cache, 1 to 300 seconds |
What a proxy cannot do: hide a secret
Anything your frontend sends, a visitor can read in DevTools, and that includes an Authorization header forwarded through Pro. The Connection key is a publishable identifier, like a Stripe publishable key: it ties traffic to your allowed origins and hosts, it is not a secret and is never forwarded to the API. Use the proxy for public data, or for tokens that belong to the signed-in user. A private API key for a paid service belongs on a backend you control, however small.
Good questions.
Is an API proxy the same as a CORS proxy?
For a frontend app, yes. A CORS proxy exists to add the Access-Control-Allow-Origin header a browser needs; an API proxy that does nothing else is a CORS proxy. The difference appears when the proxy also injects secrets, caches or transforms responses, which cors.dev does only as opt-in Pro features.
Does the proxy see my data?
Every proxy can read what passes through it, which is why the free tier refuses cookies and Authorization headers outright. Send public data through it. Pro forwards an explicit Authorization header for APIs that need one; it still never forwards cookies and never stores the header.
Which status code do I get when the API is down?
The API status when it answered at all: a 500 from the API arrives as a 500 with X-Cors-Source: upstream. When the proxy could not get an answer you receive 502 with X-Cors-Error set to upstream_unreachable, upstream_timeout after the 10 second deadline, or response_too_large above 1 MiB (6 MiB on Pro).
Which hosts can I reach?
Any public HTTPS host on port 443: no IP literals, no private networks, no http:// targets, and no domains whose owner has verified an opt-out. On a Pro Connection you list the exact hosts, redirect hops included, and the proxy refuses everything else with target_not_allowed.
Can I use it in production?
The free tier has no monthly quota, so a public widget can run on it indefinitely within the shared fair-use pool. For traffic you need guarantees for, Pro gives you 600 requests per minute, 10 concurrent requests and a usage view per key for $5 per month.
Is there an SDK?
The prefix works with fetch, axios, XMLHttpRequest, jQuery, Dart, C# or curl unchanged. If you prefer a drop-in that retries natively first and proxies only on failure, load https://cors.dev/auto.js on the page; it is documented at /docs/auto.