Skip to main content

Attach Kaleidr Chat to a Mapbox map

At the end of this guide you have a page with a Mapbox GL JS map and a chat panel docked inside it. A visitor asks a question, the answer streams into the panel, and the places in it appear as pins on your Mapbox map while the camera moves to show them. This works on every plan, including Free, with a publishable key. Questions spend your organization's AI credits.

Before you start​

  • A Kaleidr publishable key (kld_pk_live_…) from kaleidr.com/api-keys. New keys carry the ai scope that Chat needs. See Get an API key.
  • Your page's origin in the key's allowed origins, if the key lists any. For local work, add the origin you serve from, for example http://localhost:8080.
  • A Mapbox access token (pk.…) from your Mapbox account. Mapbox bills the map; Kaleidr bills the questions. The two are unrelated.
  • Mapbox GL JS loaded with a script tag, so that window.mapboxgl exists. If you import Mapbox from npm, read Making an npm-imported library a page global first.
  • A way to serve the page over HTTP, such as npx serve or python -m http.server 8080. A file opened from disk does not work.
  • If your site sends a Content Security Policy, the hosts on the Content Security Policy page allowed.

Step 1: Lay out the map and the chat layer​

Put the map and the chat mount target in one positioned wrapper. The chat layer sits on top of the map and lets pointer events through.

<div id="mapHost" style="position:relative; height:520px">
<div id="map" style="position:absolute; inset:0; z-index:0"></div>
<div id="chat" style="position:absolute; inset:0; z-index:1; pointer-events:none"></div>
</div>

You should see: nothing yet, a 520 px tall empty block. Theming and layout explains what each of those declarations does.

Step 2: Load Mapbox GL JS and create the map​

Load the stylesheet in the page head and the script before your own code. The stylesheet is required: without it the map does not display correctly.

<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/v3.9.4/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.9.4/mapbox-gl.js"></script>
<script>
mapboxgl.accessToken = 'pk.…';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-0.12, 51.5],
zoom: 11,
});
</script>

You should see: a Mapbox map of central London that you can drag and zoom.

Step 3: Load kaleidr.js​

Add the loader after the Mapbox script and before the code that mounts the panel.

<script src="https://cdn.kaleidr.com/embed/v1/kaleidr.js"></script>

You should see: no change in the page. In the console, Kaleidr.version returns a version string.

Step 4: Mount the chat panel​

Pass your key and the live map object.

const handle = Kaleidr.mount('#chat', {
product: 'chat',
publishableKey: 'kld_pk_live_…',
map,
contained: true,
});

You do not need to wait for the map's load event. Kaleidr.mount returns immediately; the panel appears once the chat bundle has loaded and your key has been exchanged for a session.

You should see: an input bar that reads Ask Kaleidr at the top-left corner of the map.

The complete page​

Save this as index.html, replace the two keys, and serve the folder over HTTP.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kaleidr Chat on Mapbox</title>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/v3.9.4/mapbox-gl.css" />
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
#mapHost { position: relative; height: 100vh; }
#map { position: absolute; inset: 0; z-index: 0; }
#chat { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
</style>
</head>
<body>
<div id="mapHost">
<div id="map"></div>
<div id="chat"></div>
</div>

<script src="https://api.mapbox.com/mapbox-gl-js/v3.9.4/mapbox-gl.js"></script>
<script src="https://cdn.kaleidr.com/embed/v1/kaleidr.js"></script>
<script>
mapboxgl.accessToken = 'pk.…';

const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-0.12, 51.5],
zoom: 11,
});

const handle = Kaleidr.mount('#chat', {
product: 'chat',
publishableKey: 'kld_pk_live_…',
map,
contained: true,
});
</script>
</body>
</html>

How it behaves​

The visitorThe panelYour Mapbox map
Sends a questionExpands into a chat window; the answer streams in as textNothing yet
WaitsShows a result card for each place in the answerA pin appears for each place as it is located. One place: the camera flies to it. Several: the camera fits them, padded so they stay clear of the panel.
Clicks a pin or a resultOpens a place card with an overview, photos and a directions toolClicking a result moves the camera to that place
Requests directionsShows the route's distance and durationA route line is drawn and framed
Asks another questionAdds to the same conversationNew pins are added; earlier pins stay
Closes the chat windowReturns to the input bar; the conversation endsThe pins and the route are removed

Things specific to Mapbox GL JS:

  • Pins are Mapbox Marker objects with an HTML element. They are separate from any markers you add.
  • The route is drawn with sources and layers that the panel adds to your style and removes again. Your own sources and layers are not changed.
  • The session is renewed in the background for as long as the panel is mounted. You do not handle tokens.

Check that it worked​

In the page

  • The Ask Kaleidr input bar is over the top-left of the map.
  • Type Show me three museums in London and press Enter. Text streams in, three pins appear, and the map fits them.

In the console

  • No error or warning that contains [kaleidr.

In the Network tab

RequestExpect
GET https://cdn.kaleidr.com/embed/v1/kaleidr.js200
GET https://cdn.kaleidr.com/embed/v1/chat.js200
POST https://api.kaleidr.com/inference-api/b2b/v1/sdk/sessions200, with a JSON body like the one below
POST https://api.kaleidr.com/inference-api/b2b/v1/chat/control/stream (after you ask)200, content-type: text/event-stream
{ "session_token": "kld_sess_live_…", "expires_in": 900, "product": "chat" }

If it does not work​

The SDK logs a short message and does not repeat the server's reason. When a request fails, open it in the Network tab and read its JSON response. A 403 or 404 carries its code in the detail field. A 429 carries its code in the error field, with a sentence in detail.

SymptomCauseFix
No panel. Console: a CORS error for …/sdk/sessions, then an error containing [kaleidr:chat]The key is unknown or revoked, or the page's origin is not in the key's allowed origins. The API only sends CORS headers to an admitted origin, so the browser hides the response.Check the key. Add the exact origin, including scheme and port, at kaleidr.com/api-keys.
No panel. Console: [kaleidr:chat] kaleidr session exchange failed (403)The key is valid here but may not open Chat. The response detail is insufficient_scope (no ai scope), product_not_allowed (the key is restricted to other products) or tier_capability_not_allowed.Use a key that carries ai and allows the chat product.
No panel. Console: [kaleidr:chat] Kaleidr: a server key (kld_sk_…) must never be used in the browser …You passed a server key. No request was made.Use a publishable key (kld_pk_live_…). If the server key was in a deployed page, revoke it.
No panel. Console: [kaleidr:chat] mount target not found: #chatThe selector matched nothing when mount ran.Run mount after the element exists, or pass the element itself.
No panel. Console: [kaleidr:chat] [kaleidr] failed to load https://cdn.kaleidr.com/embed/v1/chat.jsThe bundle was blocked, usually by a Content Security Policy or an ad blocker.Allow https://cdn.kaleidr.com in script-src.
Kaleidr is not definedYour code ran before kaleidr.js loaded, for example because the loader tag has async or defer.Load kaleidr.js with a plain script tag before your code, or run your code from the tag's load event.
Console: [kaleidr] embed is disabled — …The enable gate is holding mounts.The message names the control that set it. See kaleidr.js.
The panel is at the top-left of the browser window, not over the mapcontained is missing, or #chat is not positioned.Add contained: true and the styles from step 1.
The panel answers, but no pins appear and the map does not moveThe object passed as map was not recognized.Pass the mapboxgl.Map instance itself. See Supported maps.
The map flies to a single place, but no pins appear and several places are not framedwindow.mapboxgl does not exist. Flying to one place and drawing a route line need only the map object; pins, and fitting the view to several places, an area or a route, need the page global. Nothing is logged.If Mapbox comes from npm, set window.mapboxgl before mounting. See Making an npm-imported library a page global.
The panel shows "Sorry — that didn't go through. Please try again." on every questionThe session is no longer valid: the key was revoked, or its allowed origins changed.Check the key, then reload the page.
The panel shows "You've reached your usage limit for now."A 429: your organization's AI credits are spent, or the key's rate limit was hit. All visitors of a page share one key's rate limit.See Quota and rate limits.
The map cannot be draggedThe chat layer is covering it.Add pointer-events:none to #chat.

More symptoms are on the Troubleshooting page.

Limits​

  • Your page gets no callback or event from the panel. You cannot react to a question, an answer or a pin click in code.
  • The assistant does not see your layers, your markers or the area the map is showing. Only the zoom level is sent with a question.
  • You cannot give the assistant instructions or your own data through the panel.
  • The language of the answers, the panel's title and its input placeholder are mount options (lang, title, placeholder): see the Chat reference. The panel's other labels are in English.
  • handle.setCamera() does nothing on a chat mount and logs a warning. Move the camera with Mapbox's own API.
  • In a single-page app, call handle.destroy() when the map's view is removed. Mount again when it returns.

The full list is on Capabilities and limits.

Next steps​