إنتقل إلى المحتوى الرئيسي
This page is not available in العربية yet. You are reading the English version.

Production checklist

An integration that works on localhost can still fail in production, or cost more than you planned. Work through this list before you ship. Each item says what to do, why, and how to check it.

The short list​

Separate keys per site and environment​

Create a different key for each place your integration runs: one for production, one for staging, one for each customer-facing site. Name each key after where it runs.

Why:

  • You can revoke one without stopping the others. Revoking a key ends it and every session issued from it immediately.
  • Each key gets its own allowed origins. A production key that also lists http://localhost:3000 can be used by anyone running a page on their own machine.
  • Each key has its own request-rate budget. Every visitor using one publishable key shares that key's requests-per-minute budget. Splitting busy sites across keys keeps one from starving another. How the budgets work, and what a refused request looks like, is in Quota and rate limits.

The number of active keys is limited per plan; the limits are listed under What each plan admits. All keys of an organization still share one pool of AI credits and one pool of map loads. To fence one key's spending, set a monthly AI credit limit on it through the key API.

Check: kaleidr.com/api-keys shows one clearly named key for each site, and no key you cannot account for.

Allowed origins on every publishable key​

A publishable key with no allowed origins works from any site. That is fine on day one and wrong in production: the key is in your page source, and anyone can copy it and run Chat on their own site with your AI credits.

For each production key, list exactly the origins that serve your pages:

  • Include every variant you serve: https://example.com and https://www.example.com are different origins.
  • Include the port if it is not the default.
  • Do not include localhost on a production key. Use a separate key for development.

Check: open your production page. The product renders and the Network tab shows a 200 from …/b2b/v1/sdk/sessions. Then confirm the lock works: serve the same page from an origin that is not listed and see that nothing renders and the console reports a CORS error for that request.

The allowed-origins list stops other websites from using your key in a browser. It is not a secret-keeping mechanism: a program that is not a browser can send any Origin header. Kaleidr rate-limits each key and you can cap its monthly spend, which bounds what a copied key can cost. See Security model.

Server keys stay on servers​

A server key (kld_sk_live_…) can call the API with no session and no origin check. Keep it in your server's secret store and nowhere else.

  • Never ship one in a web page. The SDK refuses to mount with a server key, and the API refuses a server key on any request that carries an Origin header (403 server_key_in_browser), but by the time either happens the key is already in your page source.
  • Never embed one in a mobile app. App binaries can be unpacked.
  • If one leaks, revoke it. Do not rotate it: rotating from the API Keys page leaves the old value working for 24 hours.
  • Consider an IP allowlist for it, set through the key API.

Check: search your front-end bundle and your repository for kld_sk_. There should be no match outside server-side configuration.

Content Security Policy​

If your site sends a Content-Security-Policy header, each product needs entries in it. If your site sends none, skip this item.

ProductYour policy must allow
ViewerScripts from https://cdn.kaleidr.com; frames from https://map-embed.kaleidr.com
TileThe same, plus connections to https://api.kaleidr.com
ChatScripts from https://cdn.kaleidr.com; connections to https://api.kaleidr.com; inline styles; fonts from data: addresses; images for place photos and for the icons of cited sites; and whatever your own map already needs
EditorScripts from https://cdn.kaleidr.com; connections to https://api.kaleidr.com and https://tile.kaleidr.com; inline styles; images from https://tile.kaleidr.com; workers from blob: addresses

Content Security Policy has the full policy text for each product. Place photos in Chat come from third-party image hosts that vary, and the icons of cited sites from Google's favicon service, so read that page before you write a narrow img-src.

Test on the deployed site, not only locally: development servers often send no policy at all.

Check: load each page that mounts a product with the console open. There are no messages that begin Refused to load, Refused to connect, Refused to frame or Refused to apply inline style. Roll a new policy out with Content-Security-Policy-Report-Only first if you can.

If your site asks visitors for consent before loading third-party services, hold Kaleidr until they agree. The loader has an enable gate for this. While the gate is off, a mount returns a handle that does nothing: no product code is fetched, no session is requested and nothing is rendered.

What Kaleidr does in a visitor's browser​

Use this to decide what your consent notice needs to say:

  • Loading the loader, a product, or a frame makes requests to Kaleidr hosts (cdn.kaleidr.com, api.kaleidr.com, map-embed.kaleidr.com, tile.kaleidr.com).
  • Chat sends the visitor's questions to api.kaleidr.com.
  • Chat stores a small number of display preferences, such as measurement units, in the browser's localStorage.
  • Place photos in Chat load from third-party image hosts.
  • Chat loads the icon of each site an answer cites from Google's favicon service (www.google.com, redirected to gstatic.com), which is sent the cited site's address.

Capabilities and limits lists exactly what Chat sends.

Set window.__KALEIDR_EMBED_ENABLED__ to false before the loader script. Every mount on the page is then held, including <kaleidr-map> elements that are already in the HTML.

<script>
// Must be a real boolean. A string such as "false" is ignored, and the embed would mount.
window.__KALEIDR_EMBED_ENABLED__ = localStorage.getItem('consent') === 'granted';
</script>
<script src="https://cdn.kaleidr.com/embed/v1/kaleidr.js"></script>

<div id="stage" style="position:relative; height:560px">
<div id="map" style="position:absolute; inset:0"></div>
</div>

<script>
function mountKaleidr() {
Kaleidr.mount('#stage', {
product: 'chat',
publishableKey: 'kld_pk_live_…',
map: window.myMap, // your own map instance, created elsewhere on the page
contained: true,
});
}

if (window.__KALEIDR_EMBED_ENABLED__) mountKaleidr();

// Call this from your consent banner's "accept" handler.
function onConsentGranted() {
localStorage.setItem('consent', 'granted');
window.__KALEIDR_EMBED_ENABLED__ = true; // release the same control that held it
mountKaleidr();
}
</script>

For a <kaleidr-map> element that was held when the page loaded, flip the flag and then put the element back into the page so that it mounts again:

function releaseElement(el) {
window.__KALEIDR_EMBED_ENABLED__ = true;
const parent = el.parentNode;
const next = el.nextSibling;
el.remove(); // tears down the held mount
parent.insertBefore(el, next); // mounts again, now enabled
}

Three rules make the gate predictable:

  • A false is released only by the control that set it. There are three controls: the page flag above, Kaleidr.enable(false), and a per-mount enabled: false option (the flag="false" attribute on the element). If you hold with the page flag, release with the page flag. Kaleidr.enable(true) does not release a page flag or a per-mount false.
  • Any false wins. If one control says false, the mount is held, whatever the others say.
  • The page flag accepts only true or false. Never assign it a stored string directly.

While a mount is held, the console shows one warning that names the control holding it, for example:

[kaleidr] embed is disabled — window.__KALEIDR_EMBED_ENABLED__ is false. Reassigning it releases that lever — Kaleidr.enable() cannot.

The gate holds products, not the loader. The loader script itself is still fetched from cdn.kaleidr.com before consent. If your policy does not allow even that, add the loader's script tag to the page from your consent handler instead of writing it in the HTML. A plain <iframe> embed of a published map is not affected by the gate at all; add the iframe after consent.

Check: load the page in a fresh browser profile. Before you accept, the Network tab shows no request to api.kaleidr.com or map-embed.kaleidr.com. After you accept, the product mounts without a reload.

The full gate reference is in kaleidr.js.

Layout​

  • Give every <kaleidr-map> element a height. Without one, a Viewer or Tile frame falls back to 500 pixels tall. The element is display:block by default, so the height applies. Keep display:block in the style if you support Safari before 16.4, where the element stays inline and style="height:480px" alone has no effect.
  • Dock the chat panel deliberately. Without contained: true the panel is a fixed window at the top-left of the browser window. With it, the panel sits inside the mount target, which must be positioned (position: relative or similar) and have a height.
  • Check narrow screens. Load the page at phone width and confirm that the panel and the map are both usable.
  • Check your own styles. Chat and Editor add their styles to your page, and Chat sets its theme as CSS variables on the root element. See Theming and layout.
  • Tell visitors how to zoom. Viewer and Tile zoom on scroll only while Ctrl (⌘ on a Mac) is held, so that scrolling the page does not move the map.

Check: resize the window from desktop to phone width with the product mounted. Nothing overlaps your navigation and nothing is cut off.

Quota and what visitors see​

Your organization has two pools, AI credits and map loads. Both reset each billing period. Know what happens at the end of each.

When this runs outWhat stopsWhat the visitor sees
AI creditsChat answers and server API calls are refused with HTTP 429, "error": "quota_exceeded"In the chat panel, a line in the conversation: "You've reached your usage limit for now." followed by when it resets. The panel stays open.
Map loads, with TileNew Tile sessions are refused with 429 map_load_quota_exceededNothing renders in the element. The console shows [kaleidr:tile] kaleidr session exchange failed (429).
Map loads, with a published mapThe published map stops renderingText in the map's frame: "This map has reached its map load limit. It will be available again when the owner's allowance resets."

Plan for it:

  • Size the plan to the traffic. Each chat answer spends AI credits, and a request is refused when the pool cannot cover it. Each Tile session and each view of a published map on a site spends a map load.
  • Count Tile sessions correctly. A map load is counted for each Tile session, not for each page view. A session lasts about 15 minutes and the SDK renews it while the element stays mounted, so a page left open for an hour uses several. Removing a Tile element and adding it again also starts a new session. Mount a Tile once and keep it; use setStyle and setCamera to change it.
  • Leave room behind an empty Tile. Because a Tile that cannot start renders nothing, give its container a background color or a fallback image so the page does not show a blank gap.
  • Remember who else spends the pool. AI credits are shared by all of your organization's keys, by Visitor AI Chat on your published maps and by your team's own use of Kaleidr's apps.
  • Know the difference between a quota and a rate limit. Each key also has a per-minute request budget. Going over it returns 429 with "error": "rate_limited" and a Retry-After header of a few seconds. The chat panel shows the same usage-limit line for both. To tell them apart, read the error field of the response in the Network tab.
  • Extend a pool without changing plan by buying an add-on pack from the pricing page.

Check: you know your plan's two allowances, you have an estimate of monthly answers and map loads, and the page still looks acceptable with the Tile element empty.

Errors​

In the SDK​

The SDK does not throw into your code for a failed mount, and has no error callback. The one call that throws is Kaleidr.mount with no options object, which raises a TypeError. What happens instead:

  • A mount that fails logs one console error that contains [kaleidr:<product>], and renders nothing. Your page keeps running.
  • A chat answer that fails shows a line in the conversation: "Sorry — that didn't go through. Please try again." Text that had already streamed stays visible.
  • A published map that cannot load shows its reason as text in the frame and posts a kaleidr-embed:error message to your window with a code. Listen for it if you want to replace the frame with your own fallback. See Viewer embed.

Because Chat and Editor report nothing to your page, you cannot log their failures from your own code. Test the failure cases by hand before launch, using Troubleshooting as the list.

In your own code that reads a stream​

If your backend calls the stream endpoint, handle these:

  • Failures before the stream starts are ordinary HTTP responses with a JSON body: 401 for a bad key, 403 for a scope, plan or product refusal, 429 for quota or rate, 422 for a malformed request or one over a size limit, 413 for a body over 2 MiB. Check the status before you start reading events.
  • Trim the conversation you resend. A request can carry at most 400 messages and 200,000 characters of messages in total; over that it is refused with 422 too_many_messages or conversation_too_long. Keep the most recent turns and drop the oldest.
  • An aborted stream is still charged, for at least the conversation the request sent. Closing a stream early does not make a long request free, and the charge can be more than the hold placed at the start.
  • A failure during the stream arrives as an error event in place of the end event. It carries a message and the text produced so far. Show the partial text or retry; do not wait for an end that will not come.
  • The quota event is the last event, after end or error. Do not close the connection on end if you want to read it.
  • Streams are long-lived. While an answer is being prepared the server sends a keep-alive comment line about every ten seconds. Set your HTTP client's read timeout well above that, ignore lines that start with :, and do not buffer the response.
  • Back off on 429. Honor Retry-After. For quota_exceeded, trust the reset_at field in the body for when the pool resets.

Reading the stream has complete clients that do all of this, and Errors lists every code.

Teardown in single-page apps​

In an application that changes views without reloading the page, a mounted product outlives the view unless you remove it.

  • Call handle.destroy() when the view goes away: in React, from the cleanup function of the effect that mounted it; in Vue, from onBeforeUnmount. Destroying a mount removes what it rendered and stops its session renewals.
  • Removing a <kaleidr-map> element from the page destroys it for you. Adding it back mounts it again, with a new session exchange. For Tile that is a new map load.
  • Do not remount to change something. Changing the style-id, share-id or flag attribute of a mounted element does nothing. The camera attributes (center, zoom, pitch, bearing) do update a mounted Viewer, Tile or Editor element, and theme updates a mounted Chat element. On the other products each of them does nothing. To switch a Tile's style, call setStyle on the handle.
  • Destroy Chat before you remove your map. Chat holds a reference to the map object you passed and removes its pins when destroyed.
  • Expect Chat's styles to remain. After destroy(), the panel and its pins are gone, but the <style> tag Chat added and the CSS variables it set on the root element stay for the life of the page.
  • Mount one Editor per page.

Check: navigate away from and back to the view ten times. The Network tab shows one session exchange for each mount and none after you leave, and the page has one panel, not ten.

Frameworks has the mount and cleanup pattern for React, Next.js and Vue.

Test keys are not a sandbox​

A test key (kld_pk_test_…, kld_sk_test_…) calls the same API as a live key and spends the same AI credits and map loads. There is no free test mode. A test key cannot load basemap tiles.

Use test keys to keep staging traffic on a key you can revoke separately, not to avoid cost. Load tests and automated end-to-end tests against Kaleidr spend real allowance; keep them small.

Check: your staging environment does not run a load test against a Kaleidr key.

Monitor usage​

  • The Billing page in your account (kaleidr.com/billing) shows your organization's AI credit and map-load usage for the current billing period, with API usage shown separately from your team's use of Kaleidr's apps.
  • A stream ends with a quota event. It is sent on a best-effort basis, so a stream can end without one. On a stream opened with a server key it carries used and limit for the AI credit pool. A backend integration can record these and alert before the pool is spent.
  • A 429 quota_exceeded response to a server key carries limit, used, remaining and reset_at.
  • Browser sessions do not see the numbers. With a session token, the quota event and the 429 body say when the pool resets (reset_at) and leave out your plan, usage and limit, so a visitor cannot read them. Monitor usage from your backend or the Billing page.
  • The key API records each key's history: when it was created, changed, rotated and revoked. Read it at GET https://api.kaleidr.com/shared-api/api-keys/{id}/events.

Kaleidr does not send usage events or conversation transcripts to your page or your server. If you need per-conversation records, call the server API from your backend and record them there.

Check: someone on your team owns the Billing page and knows what to do when usage approaches the allowance: buy an add-on pack, upgrade, or reduce traffic.

Next steps​