Skip to main content

CORS & allowed origins

A publishable key can carry a list of allowed origins: the websites that may use it. This page explains where that list is checked, what you see in the browser when a check fails, and what the list is not.

An origin is the scheme, host and port of a page, for example https://www.example.com or http://localhost:3000. It has no path.

The list is optional​

A publishable key works with an empty list. An empty list means any origin may use the key. That is the state of a key created with no origins, and you can clear a list later.

Add origins before you put a key on a production site. With a list in place, a copy of your key pasted into someone else's web page stops working there. What that does and does not protect is covered below.

Server keys ignore the list. A server key is never usable from a browser, whatever its list says.

Writing an entry​

RuleExample
Scheme, host and optional port only. No path, query or trailing slash.https://www.example.com, not https://www.example.com/
https is required. http is accepted only for localhost, 127.0.0.1 and [::1]. The API Keys page does not accept [::1]; set it through the key management API.http://localhost:3000
Matching is exact. There are no wildcards, and a subdomain is a different origin. An entry with * in it is saved as written and matches no site.https://example.com and https://www.example.com are two entries
The port is part of the origin. Leave out a default port (443 for https, 80 for http).https://example.com, not https://example.com:443
Up to 100 entries per key. Duplicates are rejected.

Entries are stored in lower case with default ports removed, so they match what a browser sends.

A page opened from disk (file://) has no origin you can add. Serve the page from http://localhost while you develop.

Set the list on the API Keys page at kaleidr.com/api-keys, or with allowed_origins in the key management API.

Where the list is checked​

StepCheckWhen it fails
1. Session exchangeThe request's Origin header must be in the key's list, when the key has one.403 origin_not_allowed. No session is issued. A request with no Origin at all is 403 origin_required.
2. Every runtime callA session token works only from the one origin it was issued for.403 session_origin_mismatch.
3. Every runtime callThe session's origin must still be in the key's list.401. Removing an origin from a key ends the sessions already issued for it.
4. Basemap requests that carry a keyThe embedded Editor sends your publishable key with its requests to https://tile.kaleidr.com for the style, sprites and fonts. The page's site must be in the key's list for the key to be recognized.The request is treated as one without a key, which can be refused with 429 tile_access_restricted.
5. The responseThe API adds Access-Control-Allow-Origin only when the credential is valid and the origin is admitted.The browser blocks the page from reading the response.

Step 5 is ordinary CORS, and it is the reason failures look the way they do in a browser.

Preflight​

Before a cross-site POST with a JSON body, a browser sends an OPTIONS preflight. Kaleidr answers every preflight with 204 and reflects the requesting origin, because a preflight carries no credential and cannot be checked against a key. The real request is where the list applies.

Preflight response headerValue
Access-Control-Allow-MethodsGET, POST, OPTIONS
Access-Control-Allow-HeadersAuthorization, Content-Type, Accept, X-Api-Key, X-Request-Id
Access-Control-Max-Age600

A request that sends any other header fails at preflight in the browser. Do not add custom headers to calls made from a page.

On an admitted response the API also sends Access-Control-Expose-Headers: Retry-After, X-Request-Id, so page code can read those two headers. It never sends Access-Control-Allow-Credentials: the API does not use cookies.

What you see when an origin is refused​

When the origin or the credential is refused, the response has no Access-Control-Allow-Origin header. The browser then hides the response from your code. So the page does not see the 401 or 403 body. It sees a network error.

In the console:

Access to fetch at 'https://api.kaleidr.com/inference-api/b2b/v1/sdk/sessions' from origin
'https://www.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

followed by an error from the SDK that contains [kaleidr:chat] (or the product you mounted). Nothing renders.

To find the real cause, open the browser's Network tab, select the sessions request and read its status and response:

Status and code in the Network tabCauseFix
403 origin_not_allowedThis page's origin is not in the key's list.Add the exact origin shown in the request's Origin header.
401 Invalid or missing API keyThe key is mistyped, revoked or expired.Check the key on the API Keys page.
403 server_key_in_browserA server key was sent from a page.Use a publishable key in the page. Rotate the server key.
403 session_origin_mismatchA session token was used on a different origin from the one that requested it.Exchange the session on the page that uses it.
Preflight fails, no POST appearsThe request sent a header outside the allowed list.Remove the custom header.

Once the credential and origin are accepted, later errors are readable by the page and produce no CORS error. A key that is valid on this origin but not allowed this product fails the exchange with a readable 403 (product_not_allowed, insufficient_scope or tier_capability_not_allowed), and the SDK logs kaleidr session exchange failed (403). A 429 for a valid session arrives with its body and its Retry-After header. The codes are explained in Auth and scopes.

The same request never shows a CORS error from a terminal, because CORS is a browser rule. That makes curl a quick way to check a key and origin pair:

curl -i https://api.kaleidr.com/inference-api/b2b/v1/sdk/sessions \
-H "X-Api-Key: kld_pk_live_…" \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{"product":"chat"}'

An admitted origin returns 200 with access-control-allow-origin: https://www.example.com. A refused one returns 403 {"detail":"origin_not_allowed"} with no such header.

Why a server key is refused from a browser​

A server key is a standing secret. Anyone who has it can call the API as your organization until you revoke it. Page source is public, so a server key in a page is a leaked key.

Kaleidr refuses it at three points:

  1. The SDK rejects a kld_sk_… key at mount, with a console error and no network request.
  2. The API answers any server-key request that carries an Origin header with 403 server_key_in_browser.
  3. The response carries no CORS header, so page code could not read an answer in any case.

None of these undo the leak. CORS blocks the response, not the request: by the time the API refuses, the key has already been sent and is visible to anyone who views the page. If a server key has been in a page, rotate it.

The list is not a secret-keeper​

The Origin header is set honestly by browsers and by nothing else. A script, a command-line tool or a server can send any Origin it likes, as the curl example above does. So:

  • The list stops other websites from using your key in their visitors' browsers. That is its job, and it does it well.
  • The list does not stop a determined caller who copies your publishable key and sends requests with your origin from outside a browser.

Plan for that. Treat everything a publishable key can reach as available to anyone who can read your page:

  • Keep quota exposure bounded with a per-key monthly cap and the key's rate limit. See Quota and rate limits.
  • Do not put the vendor scope on a publishable key unless the data it reads is data you would publish on that page. See Auth and scopes.
  • Use one key per website, so you can revoke one without breaking the others.

The fuller picture is in the security model.

Credentials in URLs​

Platform API calls carry the credential in a header, never in a URL. Basemap requests made by Kaleidr embeds are the one exception: they carry a browser credential in the request URL, where it can appear in browser history and network logs. That is acceptable for a publishable credential, which is public already. It is never acceptable for a server key. Do not put kld_sk_… in any URL.

Changing the list​

  • Adding an origin takes effect for the next session exchange from that origin. Basemap requests can take a few minutes to accept the change.
  • Removing an origin refuses new exchanges from it and ends its existing sessions on their next request.
  • Clearing the list returns the key to "any origin".