Attach Kaleidr Chat to a Google map
At the end of this guide you have a page with a Google 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 Google 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.
Your Google Maps key and your Kaleidr key are separate. Google bills the map; Kaleidr bills the questions. Kaleidr never receives your Google key.
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 Google Maps JavaScript API key from your Google Cloud project, with the Maps JavaScript API enabled and your origin allowed in its own referrer restrictions.
- 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, in addition to the hosts Google Maps needs.
The Google Maps JavaScript API always defines the google.maps global, so there is no extra step to expose the
library to the chat bundle.
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:relative; height:100%; z-index:0"></div>
<div id="chat" style="position:absolute; inset:0; z-index:1; pointer-events:none"></div>
</div>
The map element fills the wrapper with height:100%. position:relative together with z-index:0 keeps
Google's own layers inside the map element, below the chat layer.
You should see: nothing yet, a 520 px tall empty block.
Step 2: Load kaleidr.js
Load the Kaleidr loader with a plain script tag, so that Kaleidr exists by the time the map callback runs.
<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 3: Define the callback that creates the map and mounts the panel
The Google Maps script calls a function you name when it has finished loading. Create the map and mount the
panel inside that function, because google.maps does not exist before it runs.
<script>
function initMap() {
const map = new google.maps.Map(document.getElementById('map'), {
center: { lat: 51.5, lng: -0.12 },
zoom: 11,
});
Kaleidr.mount('#chat', {
product: 'chat',
publishableKey: 'kld_pk_live_…',
map,
contained: true,
});
}
</script>
You should see: still nothing. The function is defined but nothing has called it yet.
Step 4: Load the Google Maps script, after the callback
Put the Google script tag below the script that defines initMap.
<script async
src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_KEY&callback=initMap&loading=async&v=weekly"></script>
The order matters. The Google script is async, so the browser runs it as soon as it has downloaded, and it
calls initMap right away. If the tag comes first, it can finish before the browser has reached the script that
defines initMap, and Google reports that initMap is not a function. Defining the callback first removes the
race.
You should see: a Google map of central London, with an input bar that reads Ask Kaleidr at its top-left corner.
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 Google Maps</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
#mapHost { position: relative; height: 100vh; }
#map { position: relative; height: 100%; 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>
<!-- 1. The Kaleidr loader. -->
<script src="https://cdn.kaleidr.com/embed/v1/kaleidr.js"></script>
<!-- 2. The callback, defined before the Google script can call it. -->
<script>
function initMap() {
const map = new google.maps.Map(document.getElementById('map'), {
center: { lat: 51.5, lng: -0.12 },
zoom: 11,
});
Kaleidr.mount('#chat', {
product: 'chat',
publishableKey: 'kld_pk_live_…',
map,
contained: true,
});
}
</script>
<!-- 3. The Google Maps script, last. -->
<script async
src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLE_KEY&callback=initMap&loading=async&v=weekly"></script>
</body>
</html>
How it behaves
| The visitor | The panel | Your Google 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 map pans to it and changes zoom. Several: the map fits them with a 56 px margin. |
| Clicks a pin or a result | Opens a place card with an overview, photos and a directions tool | Clicking a result moves the map 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 Google Maps:
- Pins are HTML elements drawn through a custom overlay. This needs no Map ID. If your page loads Google's
markerlibrary, the chat bundle uses advanced markers instead, and Google requires the map to have amapIdfor those. - Moving to a place is a pan followed by a zoom change, not one animated flight.
- A fit uses the same 56 px margin on every side. It does not account for the chat panel, so a result can land underneath it. The visitor can pan, or minimize the panel.
- The route is two polylines: a white casing under a colored line.
- The map is treated as flat and north-up. The assistant does not set heading or tilt.
- 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, and no error from the Google Maps API.
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 map. Console: an error from Google saying initMap is not a function | The Google script ran before the script that defines initMap. | Move the Google script tag below the callback, as in step 4. |
| No map, or a gray map with a Google error dialog | A problem with your Google key: not enabled for the Maps JavaScript API, billing not set up, or the origin not allowed. | Fix it in your Google Cloud project. This is independent of Kaleidr. |
Kaleidr is not defined inside initMap | kaleidr.js has async or defer, or comes after the Google script. | Load kaleidr.js first, with a plain script tag. |
The map shows, but no panel. Console: a CORS error for …/sdk/sessions, then an error containing [kaleidr:chat] | The Kaleidr 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] [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. |
| 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 is hidden behind the map | The map element has no stacking context of its own. | Keep z-index:0 on #map and z-index:1 on #chat. |
| The panel answers, but no pins appear and the map does not move | The object passed as map is not the google.maps.Map instance, for example a DOM element or a wrapper component. | Pass the object returned by new google.maps.Map(…). See Supported maps. |
| The panel answers and the map moves, but there are no pins | Your page loads the marker library and the map has no mapId. | Add a mapId to the map options, or do not load the marker library. |
| 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. |
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 overlays, 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. - Fits do not avoid the chat panel on Google Maps.
handle.setCamera()does nothing on a chat mount and logs a warning. Move the map with Google'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
- Theming and layout: match the panel to your site.
- Supported maps: what the assistant can do on Google Maps.
- Chat reference: every option, the handle and the errors.
- Production checklist: what to verify before you ship.