API docs

Use the managed proxy in minutes.

Use the hosted API when you want API keys, daily rate limits, request logs, and a dashboard. Self-host the same Go runtime when you want full control.

Sign in and create a key Machine-readable docs GitHub

Quickstart

Hosted API

For teams that want keys, rate limits, and a hosted control plane.

  1. Sign in with your email.
  2. Create an API key in the dashboard.
  3. Use ?key=... in browser requests, or X-API-Key server-side.
curl "https://api.corsproxy.dev/proxy?url=https://api.github.com/users/octocat&key=sk_live_..."

Self-hosted

For teams that want the same proxy flow on their own infrastructure.

git clone https://github.com/melihbirim/corsproxy
cd corsproxy
go run main.go

Read the self-hosting guide →

Core request format

All hosted proxy requests hit the public proxy endpoint with the upstream URL as a query parameter.

GET /proxy?url=https://example.com/data.json&key=sk_live_...

# or, for server-side callers
X-API-Key: sk_live_...

The service adds CORS headers, tracks usage per key, and enforces your plan's daily limit. Free-plan browser keys include localhost plus one additional allowed origin.

Client code

The same call in common languages. In the browser, put the key in the key parameter and let the browser send Origin. From a server, use the X-API-Key header and send an Origin header that matches one of the key's allowed origins, since keys are locked to them.

curl -G "https://api.corsproxy.dev/proxy" \
  --data-urlencode "url=https://api.github.com/users/octocat" \
  -H "X-API-Key: sk_live_..." \
  -H "Origin: https://app.example.com"   # one of the key's allowed origins
// In the browser: the key goes in the URL, the browser sends Origin itself.
const target = 'https://api.github.com/users/octocat';
const res = await fetch(
  `https://api.corsproxy.dev/proxy?url=${encodeURIComponent(target)}&key=sk_live_...`
);

if (!res.ok) {
  const err = await res.json().catch(() => ({}));
  // err.code is a reason key (e.g. RATE_LIMIT_EXCEEDED) when corsproxy.dev
  // rejected the call; without one, the upstream API returned the error.
  throw new Error(err.code ? `${err.code}: ${err.error}` : `Upstream returned ${res.status}`);
}
const user = await res.json();
const CORSPROXY_KEY = 'sk_live_...';

type ProxyError = { success: false; code: string; error: string };

export async function viaCorsproxy<T>(target: string, init: RequestInit = {}, ttl?: number): Promise<T> {
  const params = new URLSearchParams({ url: target, key: CORSPROXY_KEY });
  if (ttl) params.set('ttl', String(ttl)); // Pro: cache at the edge
  const res = await fetch(`https://api.corsproxy.dev/proxy?${params}`, init);
  if (!res.ok) {
    const body = (await res.json().catch(() => null)) as Partial<ProxyError> | null;
    // A reason key means corsproxy.dev rejected the call; otherwise the upstream failed.
    throw new Error(body?.code ? `${body.code}: ${body.error}` : `Upstream returned ${res.status}`);
  }
  return (await res.json()) as T;
}

const user = await viaCorsproxy<{ login: string }>('https://api.github.com/users/octocat');
import requests

resp = requests.get(
    "https://api.corsproxy.dev/proxy",
    params={"url": "https://api.github.com/users/octocat"},
    headers={
        "X-API-Key": "sk_live_...",
        # Keys are locked to allowed origins; send one of yours from the server.
        "Origin": "https://app.example.com",
    },
    timeout=30,
)
if not resp.ok:
    try:
        err = resp.json()
    except ValueError:
        err = {}
    if "code" in err:  # corsproxy.dev rejected the call
        raise RuntimeError(f'{err["code"]}: {err["error"]}')
resp.raise_for_status()  # otherwise the upstream API failed
print(resp.json())
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
)

func main() {
	q := url.Values{"url": {"https://api.github.com/users/octocat"}}
	req, _ := http.NewRequest("GET", "https://api.corsproxy.dev/proxy?"+q.Encode(), nil)
	req.Header.Set("X-API-Key", "sk_live_...")
	// Keys are locked to allowed origins; send one of yours from the server.
	req.Header.Set("Origin", "https://app.example.com")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(res.StatusCode, string(body))
}
<?php
$url = "https://api.corsproxy.dev/proxy?" . http_build_query(["url" => "https://api.github.com/users/octocat"]);
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "X-API-Key: sk_live_...",
        // Keys are locked to allowed origins; send one of yours from the server.
        "Origin: https://app.example.com",
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo $status, " ", $body, "\n";

Errors carry a reason key in code; see errors and reason keys. Using an AI coding assistant? See the MCP server.

Switching from another CORS proxy

The public hosted path is /proxy?url=.... If your current integration already sends the target URL as a url query parameter, you can usually migrate by replacing only the proxy base URL and appending your key query parameter or X-API-Key header.

Before: https://your-current-proxy.example/proxy?url=https://api.example.com/data
After:  https://api.corsproxy.dev/proxy?url=https://api.example.com/data&key=sk_live_...

The versioned path /v1/proxy remains supported, but /proxy is the compatibility path to use in client code.

Authentication

Create and manage API keys in the dashboard. Keys are shown only once when created.

Operational notes

Managed upstream headers

If a browser app should call a third-party API through corsproxy.dev without exposing the third-party credential, create the devproxy API key with an upstream rule.

POST /v1/api-keys
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "name": "OpenAI browser key",
  "allowed_origins": ["https://app.example.com"],
  "upstream_rules": [
    {
      "target_host": "api.openai.com",
      "path_prefix": "/v1/",
      "headers": {
        "Authorization": "Bearer sk-provider-secret",
        "OpenAI-Project": "proj_123"
      }
    }
  ]
}

The browser sends only the devproxy key. corsproxy.dev injects the configured upstream headers when the target host and path match.

Header overrides

Change headers per request with repeatable query parameters. reqHeaders sets headers on the request we send upstream; resHeaders sets headers on the response we return. An empty value removes the header.

https://api.corsproxy.dev/proxy?url=https%3A%2F%2Fapi.example.com%2Fdata
  &key=sk_live_...
  &reqHeaders=accept:application/json
  &resHeaders=content-type:application/json
  &resHeaders=x-frame-options:

Up to 20 of each per request. You can't change Host, Content-Length, Content-Encoding, Set-Cookie, hop-by-hop headers, or any Access-Control-* header. Managed upstream headers configured on your key are applied last, so a request can't replace them. Same syntax as corsproxy.io.

Caching (ttl)

On Pro and Enterprise, add ttl=<seconds> (1 to 86400) to a GET to cache successful (200) responses at the Cloudflare data center that served the request. The response carries X-Cache: HIT or MISS.

https://api.corsproxy.dev/proxy?url=https%3A%2F%2Fapi.example.com%2Fprices&key=sk_live_...&ttl=300

Cached entries are private to the API key and to the headers sent upstream, so a response fetched with one user's token is never served to another. Each data center keeps its own copy. Cached responses still count toward your daily request limit. Free-plan requests with ttl get a 403.

MCP server for AI assistants and agents

corsproxy.dev runs a remote Model Context Protocol server at https://api.corsproxy.dev/mcp (Streamable HTTP). Connect it to Claude Code, Cursor, or any MCP client and the assistant can call APIs through your key, check your quota, and decode errors while it builds your integration.

Claude Code:

claude mcp add --transport http corsproxy https://api.corsproxy.dev/mcp \
  --header "X-API-Key: sk_live_..." \
  --header "Origin: http://localhost:3000"

Cursor, Windsurf, and other clients (mcp.json):

{
  "mcpServers": {
    "corsproxy": {
      "url": "https://api.corsproxy.dev/mcp",
      "headers": {
        "X-API-Key": "sk_live_...",
        "Origin": "http://localhost:3000"
      }
    }
  }
}

It authenticates like any server-side call: your API key in X-API-Key, plus an Origin header that's on the key's allowed origins. Free keys always allow http://localhost:*, which is why the examples use it.

ToolWhat it does
fetch_urlSends a real request through the proxy (any method, optional headers, body, and ttl) and returns the status, CORS and rate-limit headers, and body (text up to 20,000 characters). Your key's managed upstream headers are applied, so the assistant can call an API that needs a secret without ever seeing the secret. Counts against your daily quota.
check_quotaToday's limit, used, remaining, and reset time.
explain_errorStatus, meaning, and fix for a reason key.

Because API keys can be public browser credentials, the MCP server only exposes what the key can already do. It doesn't return request logs or account data. Calls made through fetch_url count toward your quota but don't appear in the dashboard's Logs tab.

Errors and reason keys

When corsproxy.dev rejects a request, the response has a non-2xx status and a JSON body with a stable reason key in code:

{ "success": false, "code": "RATE_LIMIT_EXCEEDED", "error": "Rate limit exceeded. Your limit is 500 requests per day. Resets at Sat, 26 Sep 2026 00:00:00 GMT" }

Match on code, not on the error text, which may change. If the upstream API returns an error, we pass its status and body through unchanged, with no code field.

StatusReason keyMeaningWhat to do
400MISSING_TARGET_URLNo url query parameter.Add url=<encoded target>.
400INVALID_TARGET_URLThe target isn't a valid absolute http/https URL, or is over 2,048 characters.Encode it with encodeURIComponent and include the scheme.
400INVALID_TTLttl isn't a whole number from 1 to 86400.Use seconds, e.g. ttl=300.
400INVALID_HEADER_OVERRIDEA reqHeaders/resHeaders value is malformed, tries to change a protected header, or there are more than 20.Use name:value; see header overrides.
401MISSING_API_KEYNo API key in the X-API-Key header or key parameter.Send your key.
401INVALID_API_KEYThe key doesn't exist.Check for typos, or create a key in the dashboard.
401API_KEY_REVOKEDThe key was revoked (by you, or after 30+ days unused on Free).Create a new key.
403API_KEY_ORIGIN_NOT_ALLOWEDThe request came from a site the key isn't allowed on, or had no Origin header.Add the origin to the key's allowed origins in the dashboard.
403BLOCKED_HOSTThe target is on, or resolves to, a private or internal network (SSRF protection), or is corsproxy.dev itself.Only public URLs can be proxied.
403PROXY_LOOP_DETECTEDThe request already passed through corsproxy.dev once (it carries our internal loop marker header).Don't chain corsproxy.dev through another proxy, or proxy back into corsproxy.dev.
403TARGET_NOT_ALLOWEDThe key has an allowed-targets list and this host isn't on it.Add the host to the key's allowed target hosts in the dashboard.
403UNSAFE_TARGETThe target domain is flagged for malware or phishing by Cloudflare's security DNS.If it's a false positive, contact [email protected].
403BLOCKED_CONTENTThe upstream returned an executable file (Windows, Linux, macOS, or Android app).Executables can't be proxied.
403PRO_FEATURE_REQUIREDThe request used a Pro feature (ttl) on the Free plan.Upgrade, or drop the parameter.
403TERMS_NOT_ACCEPTEDThe account hasn't accepted the Terms of Service or Privacy Policy.Sign in to the dashboard and accept them.
403ACCOUNT_SUSPENDEDThe account is suspended.Contact [email protected].
403ACCOUNT_DELETEDThe account was deleted.Sign up again.
429RATE_LIMIT_EXCEEDEDThe account's daily limit is used up. Resets at midnight UTC.Wait for the reset, or upgrade.
429BANDWIDTH_LIMIT_EXCEEDEDThe account's daily bandwidth cap is used up (Free 250MB, Pro 10GB, Enterprise unlimited). Resets at midnight UTC.Wait for the reset, or upgrade.
413REQUEST_TOO_LARGEThe request body is over 10 MB.Send a smaller body.
502RESPONSE_TOO_LARGEThe upstream response is over 10 MB.Request a smaller resource or page the results.
502UPSTREAM_UNREACHABLEWe couldn't connect to the target (DNS, refused connection, TLS error).Check the target URL and that the API is up.

The dashboard's Logs tab shows these keys in the Result column, plus three log-only ones: OK (the call succeeded), PREFLIGHT (a browser CORS preflight), and UPSTREAM_ERROR (the API you called returned a 4xx or 5xx).

Need the full endpoint list?

Three formats — pick whichever your tooling speaks.

API keys only work on these public endpoints. Account management (keys, usage, logs, billing) happens in the dashboard and can't be done with an API key.

Want service status instead? corsproxy.dev/status shows live health + the components we depend on.

Read the blog for tutorials, or browse the glossary for definitions.