Endpoints
Every public endpoint of the Kaleidr platform API, one section each.
- Base URL:
https://api.kaleidr.com/inference-api/b2b/v1 - Credential:
Authorization: Bearer …orX-Api-Key: …. From a backend, a server key (kld_sk_live_…). From a browser, a session token (kld_sess_live_…). A publishable key is accepted only by the session exchange. See Auth and scopes. - Bodies: JSON, UTF-8. Send
Content-Type: application/jsonon everyPOST. A request body can be up to 2 MiB, or 5 MiB on the/design/*endpoints. A larger one is refused with 413request_too_large. - Errors: a 4xx or 5xx with a JSON body, described in Errors. One endpoint reports failure with a 200: see Directions.
The examples use a server key in an environment variable:
export KALEIDR_KEY="kld_sk_live_…"
Summary
| Endpoint | Purpose | Scope | Charged |
|---|---|---|---|
POST /sdk/sessions | Exchange a publishable key for a session | The product's scope | One map load for tile; otherwise nothing |
POST /sdk/share-sessions | Exchange a published map's id for a chat session | none | Nothing |
POST /chat/control/stream | Ask a question; stream the answer with places, actions and routes | ai | AI credits, by tokens used |
POST /chat/control/route | Directions between known coordinates | ai | Nothing |
GET /retrieval/geocode/forward | Turn typed text into places and addresses | ai | AI credits, flat fee |
POST /spot | What is at a point: the place, its address, its regions, and optionally its content | ai | AI credits, flat fee |
POST /spot/overview/stream | Stream a short overview of one thing from /spot | ai | AI credits, by tokens used |
GET /retrieval/poi/enrich | Deprecated, use /spot. Details and photos for one place | ai | AI credits, flat fee |
POST /chat/summary/stream | Deprecated, use /spot/overview/stream. Stream a short summary of a place or building | ai | AI credits, by tokens used |
POST /chat/button/stream | Stream an answer to one question about one place | ai | AI credits, by tokens used |
POST /design/analyze | Recommend a map design for a table of data | design | AI credits, flat fee |
POST /design/apply-direct | Build a map layer from a table and a chosen style | design | Nothing |
GET /design/styles, /design/samples, /design/themes | Design catalogs | design | Nothing |
How charges, holds and limits work is in Quota and rate limits.
Every runtime endpoint can return the errors in Errors shared by all runtime endpoints. Each section below lists only what is specific to it.
POST /sdk/sessions
Exchanges a publishable key for a short-lived session token bound to the calling origin and one product. The SDK calls this for every keyed mount. Call it yourself only from your own browser client.
Auth: publishable key. Requires an Origin header (a browser adds it).
Charge: one map load when product is tile; nothing otherwise.
| Field | Type | Required | Notes |
|---|---|---|---|
product | string | Yes | chat, editor, viewer or tile. |
curl -s 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"}'
{
"session_token": "kld_sess_live_eyJhbGciOi…",
"expires_in": 900,
"product": "chat",
"basemap_style_id": null
}
basemap_style_id is your organization's default basemap, or null when none is set.
| Status | Code | When |
|---|---|---|
| 401 | Invalid or missing API key | The key is unknown, revoked or expired. |
| 403 | publishable_key_required | The key is a server or legacy key. |
| 403 | origin_required | No Origin header. |
| 403 | origin_not_allowed | The origin is not in the key's allowed origins. |
| 403 | product_not_allowed | The key's allowed products exclude product. |
| 403 | tier_capability_not_allowed | Your plan does not include the product or its scope. |
| 403 | insufficient_scope | The key was created without the product's scope. |
| 422 | validation | product is missing or not one of the four values. |
| 429 | rate_limited | Too many exchanges for this key. Retry-After: 5. |
| 429 | map_load_quota_exceeded | tile only: the map-load allowance is used up. |
| 404 | sessions_disabled | Sessions are switched off on the platform. Contact support. |
| 503 | sessions_not_configured | A platform fault. Retry; contact support if it persists. |
The lifetime, the refresh rule and the order of these checks are in Auth and scopes: session exchange.
POST /sdk/share-sessions
Exchanges the id of a published map for a chat session, so the map's visitors can use chat with no key in the page. The map's Kaleidr AI tile in Studio must turn on the feature asked for.
Auth: none. Requires an Origin header. Charge: nothing; chat calls made with the session are
charged to the map owner's organization.
| Field | Type | Required | Notes |
|---|---|---|---|
share_id | string | Yes | 1 to 128 characters: letters, digits, _ and -. |
feature | string | No | control_tower (the default), the map's Ask Kaleidr panel, or popups, the AI chat in its popups. Each is granted only while its switch is on. |
curl -s https://api.kaleidr.com/inference-api/b2b/v1/sdk/share-sessions \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{"share_id":"your-map-id","feature":"control_tower"}'
{ "session_token": "kld_sess_live_eyJhbGciOi…", "expires_in": 300, "product": "chat" }
| Status | Code | When |
|---|---|---|
| 403 | origin_required | No Origin header. |
| 404 | not_found | No published map or share link has this id, or the share link's map is unpublished. |
| 403 | chat_disabled | The map's Kaleidr AI does not turn on the feature asked for. |
| 403 | origin_not_allowed | The page is not in the map's allowed domains. |
| 403 | owner_has_no_organization | The map's owner has no organization. |
| 403 | tier_capability_not_allowed | The owner's plan does not include chat. |
| 422 | validation | share_id is missing or has other characters, or feature is neither value. |
| 429 | rate_limited | Too many exchanges for this map or from this address. Retry-After: 5. |
| 404 | sessions_disabled | Sessions are switched off on the platform. |
| 503 | share_grant_unavailable, sessions_not_configured | A platform fault. Retry. |
See Kaleidr AI on a published map.
POST /chat/control/stream
Asks a question and streams the answer: prose as it is written, the places it mentions with coordinates, map actions, and routes. This is the endpoint behind Kaleidr Chat.
Auth: server key, session token or legacy key. Scope: ai.
Charge: AI credits, the tokens the answer used. Response: text/event-stream, described in the
SSE wire contract.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
messages | array of { role, content } | No | [] | The conversation so far, oldest first, ending with the user's new message. role is user or assistant. Messages with any other role, including system, are ignored, as are messages with empty content. The question answered is the last user message. Size limits are below. |
session_id | string | No | generated | An id for the conversation: 1 to 128 characters from letters, digits, _, ., : and -. Send your own stable id and repeat it on every turn. If you omit it, the server generates one for this request and returns it in the phase and start events. |
location | object { lat, lng } | No | none | Where the user is looking: the map center. Used to interpret "near here". |
map_zoom | number | No | none | The map's zoom level. |
lang | string | No | "en" | The language to answer in, as a BCP 47 tag such as fr or pt-BR. At most 35 characters. |
app_snapshot | object | No | none | Accepted. Not needed; leave it out. |
auto_actions | boolean | No | none | Accepted. Not needed; leave it out. |
Unknown fields are accepted and ignored. There is no field for system instructions, a persona, tools or your own context: the request carries the conversation, a location and a language.
The server does not store the transcript for you. Send the earlier turns in messages each time, together
with the same session_id.
A session_id is yours alone. The short-term context of a conversation is kept per organization, so another
organization that sends the same id neither sees nor changes your conversation. Within your own organization,
give each end user's conversation its own id.
Request limits
A request over one of these limits is refused with 422 before any answer is produced, and nothing is
charged. The body is {"detail": "<code>"}.
| Limit | Value | Code |
|---|---|---|
Messages in messages | 400 | too_many_messages |
Length of one message's content | 64,000 characters | message_too_long |
Length of all content values added together | 200,000 characters | conversation_too_long |
session_id | 1 to 128 characters: letters, digits, _, ., :, - | invalid_session_id |
lang | 35 characters | lang_too_long |
Every message counts toward these limits, including the ones that are ignored. A long conversation reaches the total length before it reaches the message count, so send the most recent turns and drop the oldest: see Reading the stream.
curl -N https://api.kaleidr.com/inference-api/b2b/v1/chat/control/stream \
-H "Authorization: Bearer $KALEIDR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"session_id": "trip-42",
"messages": [{"role": "user", "content": "Where is the Louvre, and what is next to it?"}],
"location": {"lat": 48.8566, "lng": 2.3522},
"map_zoom": 12,
"lang": "en"
}'
-N turns off curl's output buffering so frames print as they arrive. A shortened response, with illustrative
values:
event: phase
data: {"type": "phase", "phase": "thinking", "session_id": "trip-42"}
: keep-alive
event: start
data: {"type": "start", "session_id": "trip-42", "purpose": "control"}
data: {"text": "The [[Louvre Museum]] is on the right bank of the Seine"}
event: place
data: {"name": "Louvre Museum", "coordinates": {"lat": 48.8606, "lng": 2.3376}, "category": "museum"}
data: {"text": ", with the [[Tuileries Garden]] directly to its west."}
event: place
data: {"name": "Tuileries Garden", "coordinates": {"lat": 48.8635, "lng": 2.3275}, "category": "park"}
event: early_actions
data: {"type": "early_actions", "actions": [{"type": "map.fitBounds", "payload": {"coordinates": [[2.3376, 48.8606], [2.3275, 48.8635]], "padding": 50, "maxZoom": 15, "duration": 1000}}], "places_count": 2, "is_partial": true}
event: end
data: {"type": "end", "full_text": "The [[Louvre Museum]] is on the right bank of the Seine, with the [[Tuileries Garden]] directly to its west.", "places": [{"name": "Louvre Museum", "coordinates": {"lat": 48.8606, "lng": 2.3376}, "category": "museum"}, {"name": "Tuileries Garden", "coordinates": {"lat": 48.8635, "lng": 2.3275}, "category": "park"}], "actions": [], "question_type": "…", "response_time": 6.4, "total_tokens": 3120}
event: quota
data: {"type": "quota", "product": "b2b", "meter": "b2b.tokens", "tier": "pro", "used": 184223, "limit": 5000000, "reset_at": "2026-11-01T04:00:00+00:00", "period": "month"}
| Status | Code | When |
|---|---|---|
| 413 | request_too_large | The request body is larger than 2 MiB. |
| 422 | validation | The body is not valid JSON, or a field has the wrong type. |
| 422 | too_many_messages, message_too_long, conversation_too_long, invalid_session_id, lang_too_long | The request is over a request limit. |
| 429 | quota_exceeded | The AI-credit pool, or this key's monthly cap, cannot cover the call. |
The quota event in the example is what a server key receives. A stream read with a session token carries a
shorter one: see the quota event.
After the stream has started, a failure arrives as an error event, not as a status. To build a client, follow
Reading the stream.
POST /chat/control/route
Plans a route between coordinates you already have. No question is asked and no text is generated.
Auth: server key, session token or legacy key. Scope: ai.
Charge: nothing. The call still needs headroom in the AI-credit pool to be admitted.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
places | array of objects | Yes, at least two | [] | Each is { lat, lng, name? } with numeric lat and lng at the top level of the object. label is accepted in place of name. An entry without numeric lat and lng is skipped without an error. |
profile | string | No | "driving" | driving, walking, cycling or transit. Any other value routes as driving. |
raw_query | string | No | "Show directions" | The words the user typed, if any. |
The first place is the start and the last is the destination.
A place event on the chat stream nests its position: { "coordinates": { "lat": …, "lng": … } }. This
endpoint wants lat and lng directly on the object. Passing a stream place through unchanged gives you an
error answer, because no entry has usable coordinates.
curl -s https://api.kaleidr.com/inference-api/b2b/v1/chat/control/route \
-H "Authorization: Bearer $KALEIDR_KEY" \
-H "Content-Type: application/json" \
-d '{
"places": [
{"name": "Louvre Museum", "lat": 48.8606, "lng": 2.3376},
{"name": "Tuileries Garden", "lat": 48.8635, "lng": 2.3275}
],
"profile": "walking"
}'
Success is a 200 with an action (values are illustrative):
{
"action": {
"type": "map.showRoute",
"payload": {
"id": "control-directions",
"label": "Directions",
"profile": "walking",
"coordinates": [[2.3376, 48.8606], [2.3352, 48.8619], [2.3275, 48.8635]],
"distance_m": 870,
"duration_s": 640,
"turn_by_turn": [{ "text": "Walk west on Rue de Rivoli." }],
"attribution": {
"text": "© OpenStreetMap contributors",
"url": "https://www.openstreetmap.org/copyright",
"license": "https://opendatacommons.org/licenses/odbl/"
}
}
},
"route": { "…": "the full planning result the action was built from" }
}
Draw action.payload.coordinates, a list of [lng, lat] pairs. distance_m is meters and duration_s is
seconds. Show attribution.text, linked to attribution.url, wherever you draw the route.
For profile: "transit", payload.transit carries a one-seat journey: a walk, one ride, a walk. It has no
attribution at the top of the payload; the operator's credit is in payload.transit.attribution. Show it
wherever you show the journey.
This endpoint reports failure with HTTP 200
A route that cannot be planned is not a 4xx. The response is 200 with an error string and no action:
{ "error": "Route planning failed." }
This happens when the planner cannot connect the places and when planning fails for any other reason. When
fewer than two places have usable coordinates the text is
At least two resolved places are required to build a route. The text is written to be shown to a user as it
is, so branch on the presence of error, not on its wording. A transit trip that
no direct ride serves answers the same way, with the reason in words and three extra fields, reason,
covered and available.
Branch on the body, not the status:
const res = await fetch(`${API}/chat/control/route`, { method: 'POST', headers, body });
if (!res.ok) throw new Error(`route request refused: ${res.status}`); // 401, 403, 422, 429
const data = await res.json();
if (data.error) {
showMessage(data.error); // a 200 that is a failure
} else {
drawLine(data.action.payload.coordinates);
}
Authentication, scope, validation, quota and rate-limit failures are still ordinary 401, 403, 422 and 429 responses.
GET /retrieval/geocode/forward
Turns typed text into matching places and street addresses. It is built for a search box or a From/To field.
Auth: server key, session token or legacy key. Scope: ai.
Charge: AI credits, a flat fee per call, charged even when results is empty.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
q | string | Yes | 1 to 200 characters: a place name, a street address with a house number, or a typed lat,lng. | |
near_lat | number | No | −90 to 90. Ranks nearer matches first. Used only together with near_lng. | |
near_lng | number | No | −180 to 180. Used only together with near_lat. | |
limit | integer | No | 5 | 1 to 20. |
mode | string | No | A travel mode (driving, walking, cycling, transit). A short trip ranks nearby matches higher. |
curl -s -G https://api.kaleidr.com/inference-api/b2b/v1/retrieval/geocode/forward \
-H "Authorization: Bearer $KALEIDR_KEY" \
--data-urlencode "q=20 W 34th St, New York" \
--data-urlencode "limit=3"
{
"query": "20 W 34th St, New York",
"results": [
{
"lat": 40.7484,
"lng": -73.9857,
"display_name": "20 W 34th St, New York, NY 10001",
"type": "address",
"source": "…",
"id": "…",
"confidence": "exact",
"attribution": {
"dataset": "us/ny/city_of_new_york",
"license": "…",
"attribution": "Address data from the NYC Department of City Planning."
}
}
],
"attribution": {
"text": "Overture Maps Foundation, overturemaps.org",
"licenses": [
"https://cdla.dev/permissive-2-0/",
"https://www.apache.org/licenses/LICENSE-2.0",
"https://opensource.foursquare.com/places-notice-txt/",
"https://docs.overturemaps.org/attribution/"
],
"sources": [
{
"dataset": "us/ny/city_of_new_york",
"license": "…",
"attribution": "Address data from the NYC Department of City Planning."
}
]
}
}
Every result has lat, lng, display_name, type and source. The rest are present when they apply:
| Result field | Meaning |
|---|---|
id | On a street address: Kaleidr's identifier for that door. It stays the same across data updates, so you can store and compare it. |
confidence | How the result matched your text (table below). Absent on a typed lat,lng. |
approximate, method, matched_numbers, within_m, score | On an estimated address only (below). |
attribution | The terms of the dataset this one result came from: dataset, license, and where they apply license_url, attribution (the credit to show) and use_restriction. |
confidence | The result is |
|---|---|
exact | the typed number on the typed street, in the place asked for |
fuzzy | the same, through a misspelled street name |
name_exact | a place whose name is your words |
locality | the town, region or neighborhood your text names |
interpolated, nearest | an estimate for a house number that is not in the data |
partial | a longer street that begins with the one you typed |
name_partial | a place whose name shares some of your words |
ambiguous | an address in no named place and near no anchor: one of several |
Estimated addresses. When the typed house number is not in the data, the answer places it on its street:
between the nearest known numbers on the same side, at the nearest one, or along the block whose number range
holds it. That result carries approximate: true and:
| Field | Meaning |
|---|---|
method | "interpolated" (between two known numbers), "nearest" (at the one known number nearby) or "range" (along the block). |
matched_numbers | The numbers it was placed from. |
within_m | The distance in meters between those numbers, or the length of the block: the most the estimate can be off while the street is numbered in order. Absent for "nearest". |
score | From 0 to 1, how sure the estimate is. Treat a low score as a street-level answer, not a door. |
An exact match never carries these fields.
Attribution. Pass the top-level attribution on with the results. Its text is the credit to show, and
licenses are the terms the data's sources ask to travel with it. When results include street addresses,
attribution.sources lists each address dataset the answer drew on. You are bound by a dataset's
use_restriction for those results:
| Dataset | Credit to show | Use restriction |
|---|---|---|
au/countrywide (G-NAF) | G-NAF © Geoscape Australia licensed by the Commonwealth of Australia under the Open Geo-coded National Address File (G-NAF) End User Licence Agreement. | Not for generating an address, or a compilation of addresses, for sending mail unless each address is verified as able to receive mail against a source other than G-NAF; use must be consistent with the Australian Privacy Principles. |
ca/countrywide (Statistics Canada) | Adapted from Statistics Canada, National Address Register, 2025-07-29. This does not constitute an endorsement by Statistics Canada of this product. | Not to identify any individual, business or organization, or to suggest endorsement by Statistics Canada. |
us/ny/city_of_new_york | Address data from the NYC Department of City Planning. | None. |
us/ma/statewide (MassGIS) | MassGIS (Bureau of Geographic Information), Commonwealth of Massachusetts EOTSS | None (public domain). |
No match and failure look the same. A query with no matches returns 200 with "results": []. So does a
lookup that fails internally; that case is not charged. Treat an empty list as "nothing to show", not as an
error.
| Status | Code | When |
|---|---|---|
| 422 | validation | q is missing, empty or longer than 200 characters; limit or a coordinate is out of range. |
| 429 | quota_exceeded | The AI-credit pool cannot cover the call. |
POST /spot
Answers what is at a point: the place the user clicked, the nearest street address, and the regions that contain the point. Each value names the credit you must show with it. On request it adds the place's descriptive content and photos.
Auth: server key, session token or legacy key. Scope: ai.
Charge: AI credits, a flat fee per call by the most expensive thing want asks for, charged whether or not
anything was found: content or photos cost the fee of /retrieval/poi/enrich when the content is fetched
for this call; otherwise address costs the fee of /retrieval/geocode/forward. Content already fetched for an
earlier request costs nothing, so such a call that also asks for address is charged the geocode fee. A call that asks only for entities, regions, boundaries or tenants is
free. The default want includes address, so a call that leaves want out is charged the geocode fee.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
lat | number | Yes | −90 to 90. The point. | |
lng | number | Yes | −180 to 180. | |
zoom | number | No | 0 to 24. The map's zoom level. | |
hint | object | No | What the user clicked (below). | |
want | array of strings | No | ["entities", "address", "regions"] | The parts to return (below). |
lang | string | No | "en" | Language for content. Up to 16 characters. |
The point decides; the hint only picks. A hint selects among what stands at the point and never moves the
answer: a place more than 75 meters from lat, lng is not returned, whatever the hint names.
hint field | Notes |
|---|---|
id | Kaleidr's id for the place: the part of a poi: ref after the colon. |
name | The place's name, as on the map label. Up to 200 characters. Matched exactly against the places at the point, and names the place for content. With a region_level of county, state or country, it also selects the region at that level whose name matches, ignoring case and accents, from those at the point (for a state or country, also those up to 5 km from it). A name that matches none of them changes nothing. |
category | The place's category. Up to 100 characters. Used for content. |
region_level | With boundaries, return only the outline of the region at this level (city, county, state or country); without it, every region's outline. With name, the region at this level in both regions and boundaries is the one the name selects. |
layer, wiki_title, ta_location_id, yelp_business_id, data_source | Accepted. Used by Kaleidr's own apps; leave them out. |
want value | Adds |
|---|---|
entities | The clicked place, when hint names one that stands at the point. |
address | The nearest street address. |
regions | The city and county (United States only), state and country that contain the point. A point up to 5 km off a coast gets the nearest state and country. |
boundaries | regions, each with its outline (one level's only, with hint.region_level). |
content | The place's descriptive content: a summary, photos and a link. Needs a place name, from the clicked place or hint.name. |
photos | The place's photos only. Costs the same as content. |
tenants | The named places near the point, nearest first: the businesses in a building. Your app keeps the ones inside the footprint it shows. |
curl -s https://api.kaleidr.com/inference-api/b2b/v1/spot \
-H "Authorization: Bearer $KALEIDR_KEY" \
-H "Content-Type: application/json" \
-d '{
"lat": 40.7484,
"lng": -73.9857,
"zoom": 17,
"hint": {"name": "Empire State Building", "category": "landmark"},
"want": ["entities", "address", "regions"]
}'
A shortened response, with illustrative values:
{
"spot": { "lat": 40.7484, "lng": -73.9857, "zoom": 17 },
"entities": [
{
"ref": "poi:3f1c2a9e-…",
"kind": "poi",
"role": "clicked",
"name": { "value": "Empire State Building", "license": "CDLA-Permissive-2.0", "attr": "a1" },
"at": { "lat": 40.74844, "lng": -73.98566 },
"facts": {
"category": { "value": "landmark_and_historical_building", "license": "CDLA-Permissive-2.0", "attr": "a1" },
"website": { "value": "https://www.esbnyc.com", "license": "CDLA-Permissive-2.0", "attr": "a1" }
}
}
],
"address": {
"ref": "address:ka1…",
"line": { "value": "20 W 34th St, New York, NY 10001", "license": "CDLA-Permissive-2.0", "attr": "a2" },
"at": { "lat": 40.7484, "lng": -73.9857 }
},
"regions": [
{
"ref": "region:tiger:3651000",
"kind": "region",
"level": "city",
"name": { "value": "New York", "license": "LicenseRef-Public-Domain", "attr": "a3" },
"label": { "lat": 40.6976, "lng": -73.9795 }
},
{
"ref": "region:tiger:36",
"kind": "region",
"level": "state",
"name": { "value": "New York", "license": "LicenseRef-Public-Domain", "attr": "a3" },
"label": { "lat": 42.7559, "lng": -75.8099 }
}
],
"attributions": {
"a1": { "text": "© Overture Maps Foundation", "license": "CDLA-Permissive-2.0" },
"a2": { "text": "Address data from the NYC Department of City Planning.", "license": "CDLA-Permissive-2.0" },
"a3": { "text": "U.S. Census Bureau, TIGER/Line", "license": "LicenseRef-Public-Domain" }
},
"withheld": ["building_facts"]
}
| Field | Meaning |
|---|---|
spot | The point you sent: { lat, lng, zoom }. |
entities | The clicked place, then the tenants when you asked for them, or an empty list. Each has a ref, kind ("poi" for the clicked place, "tenant" for a tenant), role ("clicked" or "inside"), name, at (the place's own position, { lat, lng }) and facts (category, and the clicked place's website, when known). A tenant's ref is a poi: ref, so its overview can be streamed too. Ignore an entity of a kind you do not recognize. |
address | The nearest street address, or null: { ref, line, at }. |
regions | The regions that contain the point, smallest first. Each has a ref, kind ("region"), level (city, county, state or country), name, and label: a point for placing the region's name, { lat, lng }, or null. With want: ["boundaries"] each also has geometry, its outline as a GeoJSON geometry. |
content | Present when want has content or photos, a place name is known and content may be returned. The object /retrieval/poi/enrich returns (fields), or { photos, source, cached } for photos alone, with license and attr added like any other value. Can be null when no content could be fetched. |
attributions | The credits this response uses: { text, license }, keyed by the attr of the values. |
withheld | What this response left out because the API may not return it, by kind. An empty list when nothing was left out. |
A ref names a thing for /spot/overview/stream: poi:<id> for a place, address:<id> for an
address, region:<source>:<id> for a region (<source> is tiger, nuts or ne). Kaleidr's apps also use
building:<id>, which the API does not return. Treat a ref as an opaque string.
Show the credit of every value
Every value from a dataset is an object, { value, license, attr }: the data, its license, and a key into
attributions. Wherever you display a value, show attributions[attr].text with it, next to it or in a
credits area reachable from it. One response mixes several sources, so a single credit for the whole response
is not enough. Keep license and the credit with a value you store or pass on.
content is credited the same way, through its own attr. A value carrying live_only: true may be shown to
the current user only, never stored or passed on.
See Data licenses for what each license asks of you.
What the API does not return
Kaleidr's own apps show more at a point than the API returns, because some data is not licensed for use on other sites:
- Values under the ODbL, derived from OpenStreetMap: a building's facts (floors, height, class). A building's overview is not available either.
- Answers from third-party providers: address lookups from geocoding services, and ratings, reviews and photos from Yelp and Tripadvisor.
withheld exists so you can tell "nothing is known here" from "not available through the API". It names
kinds, never values: building_facts when the point is in a building, content when the only content for the
place may not be returned. Other entries name another part of the response the same way.
Errors for POST /spot:
| Status | Code | When |
|---|---|---|
| 413 | request_too_large | The request body is larger than 2 MiB. |
| 422 | validation | lat or lng is missing or out of range, zoom is outside 0 to 24, want has an unknown value, or a field is longer than its limit or malformed. |
| 429 | quota_exceeded | The AI-credit pool cannot cover the call. |
A part that could not be looked up comes back empty (null or []). The call is still charged.
POST /spot/overview/stream
Streams a short written overview of one thing that /spot returned, named by its ref.
Auth: server key, session token or legacy key. Scope: ai.
Charge: AI credits, the tokens used. Every overview is written for its request.
Response: text/event-stream: usually a place frame with the full name of the place, such as
"Springfield, Ohio, United States of America", then the frames of a place summary: see
Summary and button streams.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
ref | string | Yes | A ref from a /spot response, up to 128 characters, or place: (below). | |
lat, lng | number | Yes | The point you sent to /spot. | |
lang | string | No | "en" | The language to write in. Up to 16 characters. |
hint | object | For place: only | { name, category? }: the label to describe. |
ref | Overview |
|---|---|
poi:<id> | The place. |
region:<source>:<id> | The region. |
place: | A named label Kaleidr holds no record for, such as a place /spot returned no entity for. Send its name in hint.name. |
address:<id>, building:<id>, building: | Not available through the API: the stream sends one error frame. building: with no id, the building at the point, is accepted for Kaleidr's apps; through the API it gets the same single error frame. See What the API does not return. |
The thing is found again at lat, lng, so nothing in the request is taken as a fact about it. A poi: ref
more than 75 meters from the point, a region: ref that does not contain it, and place: without
hint.name also get the error frame.
curl -N https://api.kaleidr.com/inference-api/b2b/v1/spot/overview/stream \
-H "Authorization: Bearer $KALEIDR_KEY" \
-H "Content-Type: application/json" \
-d '{"ref": "poi:3f1c2a9e-…", "lat": 40.7484, "lng": -73.9857, "lang": "en"}'
A ref that cannot be described answers with this frame in place of a summary. It carries the ref and no
partial_text:
event: error
data: {"type": "error", "message": "Nothing to describe here.", "ref": "address:ka1…"}
| Status | Code | When |
|---|---|---|
| 413 | request_too_large | The request body is larger than 2 MiB. |
| 422 | validation | ref is not one of the forms above, lat or lng is missing or out of range, or lang is longer than 16 characters. |
| 429 | quota_exceeded | The AI-credit pool cannot cover the call. |
Moving from the deprecated endpoints
GET /retrieval/poi/enrich and POST /chat/summary/stream are deprecated. They answer as before until
2027-01-15 and can be removed at any time after it. Every response from them says so in its headers;
Deprecation is the day they were deprecated, 2026-10-16, as a Unix time:
Deprecation: @1792108800
Sunset: Fri, 15 Jan 2027 00:00:00 GMT
Link: </inference-api/b2b/v1/spot>; rel="successor-version"
The Link of /chat/summary/stream names /inference-api/b2b/v1/spot/overview/stream. Log the Sunset
header to find the calls you still make.
From GET /retrieval/poi/enrich to POST /spot:
| Before | After |
|---|---|
lat, lon (query) | lat, lng (body). Note the name. |
name, category | hint.name, hint.category |
lang | lang, up to 16 characters |
| A full lookup | want: ["content"] |
photos_only=true | want: ["photos"] |
radius, dataSource, ta_location_id, regionLevel | Leave them out. |
| The response body | content: the same object, with live_only added |
attribution | content.attribution |
The charge is the same or lower: /spot charges nothing for content already fetched for an earlier request.
A lookup is charged for the most expensive thing in want, so when the content is fetched for the call, adding
entities, address or regions gets them in the same call at no extra charge.
From POST /chat/summary/stream to POST /spot/overview/stream:
| Before | After |
|---|---|
A place: location, building_properties.name, .category | Call /spot with the point and hint: { name, category }, then stream the overview of the ref of the place in entities, with the same lat and lng. When entities is empty, send ref: "place:" with the same hint. |
building_properties.pid | ref: "poi:<id>" |
lang | lang, up to 16 characters |
max_tokens | Leave it out. |
A building: type: "building" and its fields | No successor. A building's overview is not available through the API. |
| The frames | The frames of a place summary, after a new place frame that names the place. |
GET /retrieval/poi/enrich
This endpoint is deprecated and can be removed after 2027-01-15. Use POST /spot with
want: ["content"]. See Moving from the deprecated endpoints.
Returns descriptive content for one place: a title, a summary, photos and a link.
Auth: server key, session token or legacy key. Scope: ai.
Charge: AI credits, a flat fee per call. The fee is charged whether or not content was found.
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
lat | number | Yes | Latitude of the place. | |
lon | number | Yes | Longitude of the place. Note the name: lon, not lng. | |
name | string | No | The place's name. Recommended: a point alone often matches nothing. | |
category | string | No | The place's category, as given on a place event. | |
radius | integer | No | 250 | Search radius in meters around the point. |
lang | string | No | "en" | Language for the content. |
photos_only | boolean | No | false | Return photos only (smaller response, below). |
dataSource, ta_location_id, regionLevel | string | No | Accepted. Hints used by Kaleidr's own panel; leave them out. |
curl -s -G https://api.kaleidr.com/inference-api/b2b/v1/retrieval/poi/enrich \
-H "Authorization: Bearer $KALEIDR_KEY" \
--data-urlencode "lat=48.8606" \
--data-urlencode "lon=2.3376" \
--data-urlencode "name=Louvre Museum" \
--data-urlencode "category=museum"
{
"source": "wikipedia",
"found": true,
"title": "Louvre",
"photos": ["https://upload.wikimedia.org/…/Louvre_Museum.jpg"],
"summary": "The Louvre is a national art museum in Paris, France…",
"rating": null,
"phone": null,
"hours": null,
"reviews": [],
"url": "https://en.wikipedia.org/wiki/Louvre",
"attribution": ["Wikipedia"]
}
| Field | Type | Meaning |
|---|---|---|
source | string | Where the content came from. "omitted" means content exists for this place but may not be returned through the API (below). |
found | boolean | false when nothing was found. The other fields are then empty. |
title | string | The place's title in the source. |
summary | string or null | A short description. |
photos | array of URLs | Can be empty. |
url | string or null | A link to the source page. |
attribution | array of strings | The names to credit, for example ["Wikipedia"]. Show them with the content. |
rating, phone, hours, reviews | Can be null or empty. Ratings and reviews from licensed providers are never returned through the API. | |
unverified | boolean | Optional. true when the match to your place is uncertain. |
With photos_only=true the response is { "photos": […], "source": "…", "cached": false }.
Withheld content. Some content is licensed for Kaleidr's own apps only. When that is the only content
available for a place, the response is 200 with "source": "omitted", "found": false and no content. The
call is still charged. See Data licenses.
| Status | Code | When |
|---|---|---|
| 422 | validation | lat or lon is missing or not a number. |
| 429 | quota_exceeded | The AI-credit pool cannot cover the call. |
| 500 | An internal error occurred. Please try again. | The lookup failed. The call is still charged. |
POST /chat/summary/stream
This endpoint is deprecated and can be removed after 2027-01-15. Use
POST /spot/overview/stream. A building summary has no successor. See
Moving from the deprecated endpoints.
Streams a short written summary of one place or one building. It backs the place card in Kaleidr Chat.
Auth: server key, session token or legacy key. Scope: ai.
Charge: AI credits, the tokens used. Every summary is written for its request.
Response: text/event-stream. See Summary and button streams.
The body's type selects the kind of summary.
A building ("type": "building"):
| Field | Type | Required | Notes |
|---|---|---|---|
type | string | Yes | "building". |
address | string | Yes | The building's full address. Up to 500 characters. |
lat, lng | number | No | The building's position. |
building_levels | integer | No | Number of floors, if you know it. |
building_height_m | number | No | Height in meters, if you know it. |
tenant_names | array of strings | No | Businesses in the building. Up to 100 names of up to 200 characters each. |
poi_count | integer | No | How many places the building holds. |
lang | string | No | Default "en". Up to 35 characters. |
curl -N https://api.kaleidr.com/inference-api/b2b/v1/chat/summary/stream \
-H "Authorization: Bearer $KALEIDR_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "building", "address": "20 W 34th St, New York, NY 10001", "lat": 40.7484, "lng": -73.9857}'
event: start
data: {"type": "start", "address": "20 W 34th St, New York, NY 10001"}
data: {"text": "A landmark office tower in Midtown Manhattan"}
event: end
data: {"type": "end", "full_text": "A landmark office tower in Midtown Manhattan…", "parsed": {"…": "…"}, "total_tokens": 910}
event: quota
data: {"type": "quota", "product": "b2b", "meter": "b2b.tokens", "tier": "pro", "used": 185133, "limit": 5000000, "reset_at": "2026-11-01T04:00:00+00:00", "period": "month"}
A place (type absent, or "poi"). The body accepts location (object { lat, lng }),
building_properties (object), lang and max_tokens (integer). This variant is shaped for Kaleidr's own
panel, and the meaning of each field is not a published contract yet. Prefer
/chat/control/stream to ask about a place from your own code.
| Place field | Limit |
|---|---|
building_properties.name, .type, .category, .poi_type | 200 characters each |
building_properties.pid | 128 characters |
Each text field of building_properties.event | 4,000 characters |
lang | 35 characters |
max_tokens | A value above the endpoint's default is lowered to the default. You cannot ask for a longer summary. |
Both variants read only the fields listed here. Any other field in the body is ignored. A field over its
length limit is refused with 422 field_too_long, before any summary is produced.
| Status | Code | When |
|---|---|---|
| 413 | request_too_large | The request body is larger than 2 MiB. |
| 422 | field_too_long | A field is longer than its limit, or tenant_names has more than 100 entries. The body is {"detail": "field_too_long"}. |
| 500 | Summary stream error | The body could not be read, or a building request has no address. This endpoint answers any other malformed body with 500, not 422. |
| 429 | quota_exceeded | The AI-credit pool cannot cover the call. |
POST /chat/button/stream
Streams the answer to one question about one place, such as "What are the opening hours?". It backs the question buttons on the place card in Kaleidr Chat. It has no conversation: each call stands alone.
Auth: server key, session token or legacy key. Scope: ai.
Charge: AI credits, the tokens used.
Response: text/event-stream. See Summary and button streams.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
query | string | Yes | The question. Up to 4,000 characters. | |
place_name | string | No | The place the question is about. Up to 200 characters. | |
place_type | string | No | Its category. Up to 200 characters. | |
address | string | No | Its address. Up to 500 characters. | |
location | object { lat, lng } | No | Its position. | |
lang | string | No | "en" | Language to answer in. Up to 35 characters. |
description | string | No | A description of the place you already hold. Up to 4,000 characters. | |
website | string | No | The place's website, if you know it. Up to 2,048 characters. | |
stores_preview | array of objects { name } | No | Businesses at the place, if you already hold them. Up to 50 entries; each name up to 200 characters. Other members of an entry are ignored. | |
button_id, session_id, place_id | string | No | Your own identifiers, for correlation. Up to 128 characters each. |
Fields that are not in this table are ignored. A field over its length limit is refused with 422
field_too_long, before any answer is produced.
curl -N https://api.kaleidr.com/inference-api/b2b/v1/chat/button/stream \
-H "Authorization: Bearer $KALEIDR_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What is this place known for?",
"place_name": "Louvre Museum",
"place_type": "museum",
"location": {"lat": 48.8606, "lng": 2.3376}
}'
event: start
data: {"type": "start", "query_type": "…"}
data: {"text": "The Louvre is best known for"}
event: end
data: {"type": "end", "full_text": "The Louvre is best known for…", "reply": "The Louvre is best known for…", "query_type": "…", "first_token_ms": 820, "total_ms": 3900, "total_tokens": 640}
| Status | Code | When |
|---|---|---|
| 413 | request_too_large | The request body is larger than 2 MiB. |
| 422 | field_too_long | A field is longer than its limit, or stores_preview has more than 50 entries. The body is {"detail": "field_too_long"}. |
| 500 | Button chat stream error | The body could not be read or has no query. This endpoint answers any other malformed body with 500, not 422. |
| 429 | quota_exceeded | The AI-credit pool cannot cover the call. |
POST /design/analyze
Looks at the columns and a sample of a table and recommends how to map it.
The Editor embed does not call the design endpoints. They are a REST surface for your own tools.
Auth: server key, session token or legacy key. Scope: design.
Charge: AI credits, a flat fee per successful call. A failed call is not charged.
| Field | Type | Required | Notes |
|---|---|---|---|
columns | array of strings | Yes | 1 to 256 column names. Each must be unique. |
sampleRows | array of objects | No | Up to 1,000 rows, each an object keyed by column name. |
datasetName | string | No | Up to 512 characters. |
rowCount | integer | No | Total rows in the full table, 0 or more. |
userGoal | string | No | Up to 1,000 characters: what the map should show. |
supportedStyles | array of strings | No | Style ids to choose from, as lower-case words joined by dashes. |
Field names are camelCase. Unknown fields are ignored.
curl -s https://api.kaleidr.com/inference-api/b2b/v1/design/analyze \
-H "Authorization: Bearer $KALEIDR_KEY" \
-H "Content-Type: application/json" \
-d '{
"datasetName": "stores.csv",
"columns": ["name", "lat", "lng", "revenue"],
"sampleRows": [
{"name": "Downtown", "lat": 40.7128, "lng": -74.0060, "revenue": 120000},
{"name": "Uptown", "lat": 40.7831, "lng": -73.9712, "revenue": 86000}
],
"rowCount": 240,
"userGoal": "Show which stores earn the most"
}'
{
"datasetProfile": { "…": "what was detected in each column" },
"designPlan": { "…": "the recommended style, theme and reasoning" },
"issues": [],
"recommendation": { "…": "…" },
"suggestedConfig": { "…": "…" },
"explanation": "…"
}
Read designPlan. It can be null. recommendation, suggestedConfig and explanation are older fields
kept for compatibility and are due to be removed; do not build on them. issues lists problems found in the
data.
| Status | Code | When |
|---|---|---|
| 413 | request_too_large | The request body is larger than 5 MiB. The same limit applies to /design/apply-direct. |
| 422 | validation | columns is missing, empty, too long or has duplicates; a limit above is exceeded. |
| 429 | quota_exceeded | The AI-credit pool cannot cover the call. |
| 500 | map_design_analyze_failed | The analysis failed. Not charged. |
POST /design/apply-direct
Builds a map layer from a table and a chosen style, without analysis.
Auth: server key, session token or legacy key. Scope: design. Charge: nothing.
| Field | Type | Required | Notes |
|---|---|---|---|
style | string | Yes | A style id, 1 to 64 characters. Ids are listed by /design/styles. |
config | object | Yes | An object with location, metric and visual members. location.mode ("coordinates" or "region") and visual.layerName are required; the other sub-fields are not documented yet. |
table | object | Yes | { fileName?, columns[], rows[] }. Each row is an object of column name to value, and every value must be a JSON string: send "120000", not 120000. A row with a number or null is refused with a 422. |
layerLabel | string | Yes | 1 to 512 characters. The layer's display name. |
themePackId | string | No | A theme id from /design/themes. An unknown id is ignored. |
datasetSource | object | No | { label, url? }: who published the data, for the layer's citation. |
datasetId | string | No | Up to 128 characters. Used as the layer's id. |
year | integer | No | 1800 to 2100. |
The response always has a status:
{ "status": "applied", "message": null, "spec": { "…": "a partial map specification" } }
status | Meaning |
|---|---|
applied | spec holds the layer. |
unsupported-style | The style cannot be built from this payload. message says why. |
insufficient-data | The table lacks what the style needs, for example position columns. message says what. |
All three are 200 responses. Check status.
GET /design/styles, /design/samples, /design/themes
Three read-only catalogs: the available map styles, worked sample maps, and theme packs.
Auth: server key, session token or legacy key. Scope: design. Charge: nothing. These calls count
toward the key's requests per minute but not toward concurrency.
curl -s https://api.kaleidr.com/inference-api/b2b/v1/design/styles \
-H "Authorization: Bearer $KALEIDR_KEY"
Each response wraps its list in one key:
{ "styles": [ { "…": "…" } ] }
/design/samples returns { "samples": [ … ] } and /design/themes returns { "themes": [ … ] }.
Errors shared by all runtime endpoints
Every endpoint except the two session exchanges can return these, before any endpoint-specific work is done. They are checked in this order.
| Order | Status | Code | When |
|---|---|---|---|
| 1 | 401 | Invalid or missing API key | No credential, or one that is unknown, revoked or expired. |
| 2 | 401 | publishable_requires_session | A publishable key was sent. Exchange it first. |
| 2 | 403 | server_key_in_browser | A server key arrived with an Origin header. |
| 2 | 403 | session_origin_mismatch | A session token used from another origin. |
| 2 | 403 | ip_not_allowed | A server or legacy key with an IP allowlist, called from outside it. |
| 3 | 403 | insufficient_scope | The key lacks the endpoint's scope. |
| 3 | 403 | tier_capability_not_allowed | The key has the scope; your plan no longer includes it. |
| 3 | 403 | product_not_allowed | A server or legacy key limited to certain products called an endpoint of another product. See Restricting a key. |
| 4 | 429 | quota_exceeded | Charged endpoints only: the pool or the key's cap cannot cover the call. |
| 5 | 429 | visitor_limit_reached | Published-map sessions only: this visitor has asked too much. |
| 6 | 429 | rate_limited | The key's requests per minute are used up. |
| 7 | 429 | b2b_overloaded, org_overloaded | Too many calls in progress. |
A request body over the size limit is refused with 413 request_too_large: 2 MiB on every endpoint, 5 MiB on
the /design/* endpoints. When the request declares its length, this refusal comes before every check above.
Bodies and handling for each are in Errors and Quota and rate limits.
Related
- Server quickstart: the first call, step by step.
- SSE wire contract: every event on the streams.
- Reading the stream: complete clients.
- Data licenses: the attribution you must show.