Docs/Reference/CORS

CORS

Calling the API directly from browser JavaScript — what is allowed, what fetch can read back, and why a missing key looks like a network error.

Every endpoint answers cross-origin requests from any origin, so browser JavaScript can call the API without a proxy in front of it.

js
const res = await fetch(
  'https://api.apiverve.com/v1/currencyconverter?from=USD&to=EUR&amount=100',
  { headers: { 'x-api-key': KEY } }
);
const { data } = await res.json();

Nothing has to be configured for that to work — that call hits currency converter and comes back like any other. The rest of this page is the two places it stops working.

What the API sends

Preflight OPTIONS requests are answered at the edge and never reach the API itself:

http
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, HEAD
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

Max-Age is 20 days, so a browser sends the preflight once and then calls straight through for as long as the tab lives.

Do not set credentials: 'include'

The API answers with Access-Control-Allow-Origin: *. Browsers refuse to combine a wildcard origin with a credentialed request, so adding credentials: 'include' turns a working call into a CORS failure. Authentication is the x-api-key header — there is no cookie to send.

Reading response headers from JavaScript

By default fetch can only see a handful of response headers cross-origin. The API opts the credit metadata in explicitly, so this works:

js
const res = await fetch(url, { headers: { 'x-api-key': KEY } });
console.log(res.headers.get('x-api-remaining-credits')); // "9814"
console.log(res.headers.get('x-api-max-credits'));       // "100000"

Exactly four headers are exposed, and this is the whole list:

HeaderReads as
x-api-remaining-creditsCredits left in the current cycle
x-api-credits-usedCredits spent in the current cycle
x-api-max-creditsThe cycle's allowance
x-api-versionVersion of the endpoint that answered
The rate-limit headers are not readable from a browser

x-rate-limit-limit, x-rate-limit-remaining and x-rate-limit-reset are on the wire, but they are not in the exposed set, so headers.get() returns null for them in browser JavaScript. That is the browser hiding them, not the API omitting them — the same call from a server reads them fine. If you need to pace a browser client, count your own calls against the ceiling on rate limits rather than reading it back.

When a missing key looks like a network error

This is the one CORS problem worth knowing in advance.

A request with no x-api-key header is rejected at the edge, and that rejection is an HTML 403 that carries no CORS headers at all. The browser cannot read a response it was never allowed to see, so it reports the only thing it can:

code
TypeError: Failed to fetch

No status code, no body, nothing in the console but a network failure — for what is really a missing header. A wrong key behaves completely differently: it reaches the API, comes back as a normal 401 with CORS headers and a JSON body, and your error handling works as written.

Debugging an opaque fetch failure

Check that the header is actually being sent before you look anywhere else. An undefined environment variable produces x-api-key: undefined, which is a wrong key and gives a clean 401; a conditional that skipped the header entirely gives the opaque failure above. Confirm it in the Network tab's request headers, then reproduce with curl to see the real status.

Putting a key in frontend code

A key in browser JavaScript is a key you have published. Anyone can read it from the bundle or the Network tab. That is sometimes fine and sometimes not:

  • Fine — a prototype, an internal tool, or a low-cost endpoint behind a key you are willing to rotate.
  • Not fine — your production key. Use a sub-key for the frontend so revoking it costs you nothing, and keep the account's primary key server-side. Key scoping can pin that sub-key to the one endpoint the page calls, which bounds what a lifted key is worth.

For anything user-facing, the safer shape is a thin endpoint of your own that holds the key and forwards the call. You get to rate-limit per user, cache repeated lookups, and rotate the key without shipping a new bundle. See security.

Next

Header-by-header detail is in request headers. Status codes and retry behaviour are in errors.

If the reason you are calling from the browser is a form or a lookup widget, embedded forms do it without a key on the page at all, and mock endpoints let a front end develop against the shape before the real call exists.

Was this page helpful?

Last updated