Attach Kaleidr Chat to a MapLibre map
At the end of this guide you have a page with a MapLibre 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 MapLibre 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.
The panel works with whatever style your map already uses. It does not change your style or your tile source.
Before you start
- A Kaleidr publishable key (
kld_pk_live_…) from kaleidr.com/api-keys. New keys carry theaiscope 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 MapLibre style URL. The examples use MapLibre's public demo style; use your own in production.
- MapLibre GL JS loaded with a script tag, so that
window.maplibreglexists. If you import MapLibre from npm, read Making an npm-imported library a page global first. - A way to serve the page over HTTP, such as
npx serveorpython -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 MapLibre GL JS and create the map
Load the stylesheet in the page head and the script before your own code. Both URLs name the same version.
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.17.0/dist/maplibre-gl.css" />
<script src="https://unpkg.com/maplibre-gl@5.17.0/dist/maplibre-gl.js"></script>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json', // your own style here
center: [-0.12, 51.5],
zoom: 11,
});
</script>
You should see: a map that you can drag and zoom. The demo style is a simple country map with little detail at city level; the pins and camera moves work on it all the same.
Step 3: Load kaleidr.js
Add the loader after the MapLibre 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 key, 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 MapLibre</title>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.17.0/dist/maplibre-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://unpkg.com/maplibre-gl@5.17.0/dist/maplibre-gl.js"></script>
<script src="https://cdn.kaleidr.com/embed/v1/kaleidr.js"></script>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
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 visitor | The panel | Your MapLibre map |
|---|---|---|
| Sends a question | Expands into a chat window; the answer streams in as text | Nothing yet |
| Waits | Shows a result card for each place in the answer | A 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 result | Opens a place card with an overview, photos and a directions tool | Clicking a result moves the camera to that place |
| Requests directions | Shows the route's distance and duration | A route line is drawn and framed |
| Asks another question | Adds to the same conversation | New pins are added; earlier pins stay |
| Closes the chat window | Returns to the input bar; the conversation ends | The pins and the route are removed |
Things specific to MapLibre GL JS:
- Pins are MapLibre
Markerobjects 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 Londonand 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
| Request | Expect |
|---|---|
GET https://cdn.kaleidr.com/embed/v1/kaleidr.js | 200 |
GET https://cdn.kaleidr.com/embed/v1/chat.js | 200 |
POST https://api.kaleidr.com/inference-api/b2b/v1/sdk/sessions | 200, 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.
| Symptom | Cause | Fix |
|---|---|---|
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: #chat | The 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.js | The bundle was blocked, usually by a Content Security Policy or an ad blocker. | Allow https://cdn.kaleidr.com in script-src. |
Kaleidr is not defined | Your 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 map | contained 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 move | The object passed as map was not recognized. | Pass the maplibregl.Map instance itself. See Supported maps. |
| The map flies to a single place, but no pins appear and several places are not framed | window.maplibregl 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 MapLibre comes from npm, set window.maplibregl 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 question | The 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 dragged | The 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 MapLibre's own API.- In a single-page app, call
handle.destroy()when the map's view is removed. Mount again when it returns. - The chat panel does not supply a basemap. To show a Kaleidr basemap, use the Tile product, which
renders in its own element; a chat mount can drive that element through
map-target.
The full list is on Capabilities and limits.
Next steps
- Theming and layout: match the panel to your site.
- Chat reference: every option, the handle and the errors.
- Production checklist: what to verify before you ship.
- Frameworks: mounting from React, Next.js and Vue.