Errors
This is the complete list of errors from the Kaleidr platform API. Each has an HTTP status and a code: a
short stable string you branch on. The status alone is not enough. 403 covers many different causes, and
429 covers both "slow down" and "you are out of quota".
Start with the shapes, because the code is not always in the same field.
Response shapes
| Shape | Looks like | Used for |
|---|---|---|
| Wrapped code | {"detail": "origin_not_allowed"} | Almost every 401, 403, 404, 413, 500 and 503, the 422 request-limit codes, and two 429s (map_load_quota_exceeded, and ingest_rate_limited from the upload API). The code is the detail string. |
| Flat object | {"error": "rate_limited", "detail": "API key rate limit exceeded. Slow down."} | Every other 429. The code is error; detail is a sentence for a person. Quota errors add more fields. |
| Validation list | {"detail": [{"loc": ["body", "product"], "msg": "…", "type": "…"}]} | 422 from a malformed body or query. detail is an array, one entry per problem. |
| Error at 200 | {"error": "Route planning failed."} | POST /chat/control/route only. See below. |
A function that reads the code from any of them:
async function kaleidrError(res) {
const body = await res.json().catch(() => ({}));
const code =
typeof body.error === 'string' ? body.error : // flat 429
typeof body.detail === 'string' ? body.detail : // wrapped code
Array.isArray(body.detail) ? 'validation_error' : // 422
'unknown_error';
return { status: res.status, code, body, retryAfter: Number(res.headers.get('retry-after')) || null };
}
Two headers matter: Retry-After on 429 responses, in seconds, and X-Request-Id when present, which
identifies the request if you contact support.
Authentication: 401
The API does not accept the credential.
| Code | When it happens | What to do |
|---|---|---|
Invalid or missing API key | No credential was sent, or the key is unknown, revoked or expired. Also: a session token that has expired; a session whose key was revoked or rotated without grace; a session whose origin or product was removed from its key; a session issued before a plan downgrade; an organization that is suspended or has no plan. | Check the key and its status at kaleidr.com/api-keys. For a session, exchange a new one. If a valid key keeps failing, retry after a moment: a brief platform fault also answers this way. |
publishable_requires_session | A publishable key (kld_pk_…) was sent to a runtime endpoint. | In a browser, let the SDK exchange it, or call POST /sdk/sessions first. On a backend, use a server key. |
Authorization: 403
The API knows the credential and refuses this request.
| Code | When it happens | What to do |
|---|---|---|
insufficient_scope | The key was created without the scope this endpoint or product needs. | Use a key that has the scope. Scopes cannot be added to an existing key. |
tier_capability_not_allowed | The key has the scope or product, but your organization's current plan does not include it. Also returned for a published map whose owner's plan does not include chat. Not returned for vendor: a key that carries vendor on a plan below Enterprise is not refused, and its answers are built without your data. | Change plan, or use a product the plan includes. |
server_key_in_browser | A server key (kld_sk_…) was sent with an Origin header. | Move the call to your backend. If the key was in page source, rotate it. |
session_origin_mismatch | A session token was used from an origin other than the one it was issued for, or a published-map session was used with no Origin. | Exchange the session on the page that uses it. |
ip_not_allowed | A server or legacy key with an IP allowlist was called from an address outside it, or from an address that could not be established. | Call from an allowed address, or change the key's allowed_ips. |
publishable_key_required | POST /sdk/sessions was called with a server or legacy key. | Use a publishable key for the exchange. |
origin_required | A session exchange had no Origin header. | Make the exchange from a browser page. |
origin_not_allowed | The page's origin is not in the key's allowed origins, or not in the published map's allowed domains. | Add the exact origin to the key, or the domain to the map. |
product_not_allowed | The key is limited to certain products and this request is for another one. At the session exchange: the requested product is not in the publishable key's allowed products. On a runtime endpoint: a server or legacy key called an endpoint of a product outside its allowed products, for example a key limited to tile calling /chat/*. | Change the key's allowed_products, or use another key. |
chat_disabled | Published-map exchange: the map's Kaleidr AI does not turn on the feature asked for. | Turn on Control Tower or Popup Window in the map's Kaleidr AI tile in Studio. |
feature_not_granted | A published-map session opened for popup chat was used on /chat/control/stream. Each feature has its own session. | Exchange a session with "feature": "control_tower", which the map grants while Control Tower is on. |
owner_has_no_organization | Published-map exchange: the map's owner has no organization to charge. | The owner must create or join an organization. |
Not found: 404
| Code | When it happens | What to do |
|---|---|---|
not_found | Published-map exchange: no published map or share link has this share_id. | Check the id and that the map is published. |
sessions_disabled | Session exchange is switched off on the platform. | Not something you can change. Contact support. |
Not Found | The path does not exist. | Check the URL against Endpoints, including the /inference-api/b2b/v1 prefix. |
Validation: 422
The body or query string does not match what the endpoint accepts. detail is an array; each entry's loc
names the field and msg says what is wrong.
{
"detail": [
{ "type": "missing", "loc": ["body", "product"], "msg": "Field required", "input": {} }
]
}
Fix the request; do not retry it unchanged. Two streaming endpoints, /chat/summary/stream and
/chat/button/stream, answer a malformed body with 500 instead of 422, except for a field that is too long
(below).
Request limits: 422 with a code
A well-formed request that is over a size limit is also a 422, but with the wrapped shape: detail is the code
as a string, not an array. Nothing is charged.
{ "detail": "conversation_too_long" }
| Code | Endpoint | When it happens | What to do |
|---|---|---|---|
too_many_messages | /chat/control/stream | messages has more than 400 entries. | Send the most recent turns only. |
message_too_long | /chat/control/stream | One message's content is longer than 64,000 characters. | Shorten the message. |
conversation_too_long | /chat/control/stream | The content of all messages together is longer than 200,000 characters. | Drop the oldest turns and send again. |
invalid_session_id | /chat/control/stream | session_id is longer than 128 characters or has a character other than a letter, a digit, _, ., : or -. | Use an id in that form, such as a UUID. |
lang_too_long | /chat/control/stream | lang is longer than 35 characters. | Send a BCP 47 tag such as pt-BR. |
field_too_long | /chat/summary/stream, /chat/button/stream | A text field is longer than its limit, or a list has more entries than its limit. | Shorten the field. The limits are in Endpoints. |
The limits for the chat stream are in Endpoints.
Request too large: 413
| Code | When it happens | What to do |
|---|---|---|
request_too_large | The request body is larger than 2 MiB, or larger than 5 MiB on a /design/* endpoint. The body is {"detail": "request_too_large"}. Nothing is charged. | Send less: trim the conversation, or the sample rows of a design request. Do not retry the same body. |
request_body_too_large | A request to the key management or business-data routes under /shared-api is larger than 1 MiB, or larger than 32 MiB on the upload call POST /shared-api/datahub/ingests. | Send a smaller body. For an upload, see Upload your business data. |
A request that declares a size far above any endpoint's limit can be refused at the network edge, before it reaches the API. That response has no Kaleidr JSON body, so there is no code to read: send less.
A request refused for its size, like any request refused before it is processed, is not charged and holds no AI credits or concurrency slot afterward.
Quota and rate limits: 429
Read the body before you decide whether to retry.
| Code | Field | Retry-After | When it happens | What to do |
|---|---|---|---|---|
quota_exceeded | error | seconds to reset_at | Your organization's AI credits cannot cover the call, or the key's own monthly cap would be exceeded. Also returned by the mapping-proposal call of the upload API. | Do not retry. Wait until reset_at, add credits, or raise the key's cap. |
rate_limited | error | 5 | The key's requests per minute are used up. Also: too many session exchanges for a key, a published map or one address. | Wait Retry-After seconds, then retry with jitter. |
org_overloaded | error | 5 | Your organization has too many calls in progress at once. | Wait for a call to finish, then retry. Close streams you no longer need. |
b2b_overloaded | error | 5 | The API is at capacity for all callers. | Wait Retry-After seconds, then retry with backoff. |
limiter_unavailable | error | 5 | The rate limiter could not answer. | Retry shortly. This is not about your rate. |
visitor_limit_reached | error | 5 | One visitor of a published map's chat has asked too much, per minute or per day. | Tell the visitor to try again later. |
map_load_quota_exceeded | detail | none | POST /sdk/sessions for tile: the organization's map loads are used up. | Do not retry. Wait for the period to reset or add a map-load pack. |
The quota_exceeded body carries the numbers:
{
"error": "quota_exceeded",
"detail": "Usage limit reached. Please try again later.",
"product": "b2b",
"meter": "b2b.tokens",
"tier": "pro",
"limit": 5000000,
"used": 4999100,
"remaining": 900,
"reset_at": "2026-11-01T04:00:00+00:00",
"period": "month",
"reset_in_seconds": 1296000
}
That is the body a server key receives. A request made with a session token (a session exchanged from a publishable key, or a published map's visitor session) receives a shorter body that does not disclose the organization's plan or usage:
{
"error": "quota_exceeded",
"detail": "Usage limit reached. Please try again later.",
"meter": "b2b.tokens",
"reset_at": "2026-11-01T04:00:00+00:00",
"reset_in_seconds": 1296000
}
Each field, the per-key cap variant and worked handling code are in Quota and rate limits.
Server errors: 5xx
| Status | Code | When it happens | What to do |
|---|---|---|---|
| 500 | An internal error occurred. Please try again. | An unexpected failure. | Retry once after a short wait. |
| 500 | map_design_analyze_failed | POST /design/analyze could not analyze the table. Not charged. | Check the table's columns and sample rows; retry. |
| 500 | Summary stream error | POST /chat/summary/stream could not start: a body that cannot be read, or a building request with no address. A field that is too long is a 422 field_too_long instead. | Fix the body. |
| 500 | Button chat stream error | POST /chat/button/stream could not start: a body that cannot be read or has no query. A field that is too long is a 422 field_too_long instead. | Fix the body. |
| 503 | sessions_not_configured | Session exchange is unavailable because of a platform fault. | Retry; contact support if it persists. |
| 503 | share_grant_unavailable | Published-map exchange: the map's settings could not be read. | Retry shortly. |
An error with status 200
POST /chat/control/route returns 200 when a route cannot be planned:
{ "error": "Route planning failed." }
The response has error and no action. A client that checks only the status treats this as success and
then fails reading action. Always check for error in the body. The text is written to be shown to a user.
Details: Directions.
GET /retrieval/geocode/forward has a related behavior: a lookup that fails internally returns 200 with an
empty results list, the same as a query with no matches.
Errors on a stream
Everything above applies before a stream starts: a refused streaming request is an ordinary HTTP error. Once
the response is 200 and frames are flowing, a failure arrives as an error event:
event: error
data: {"type": "error", "code": "upstream_stall", "message": "The AI service stopped responding. Please try again.", "partial_text": "The Louvre sits"}
| Stream | code | When it happens | What to do |
|---|---|---|---|
/chat/control/stream | upstream_stall | The answer stopped making progress, or the stream reached its maximum length of about 10 minutes. | Offer a retry. Keep partial_text if it is useful. |
/chat/control/stream | absent | Any other failure while answering. | Show message. A retry may succeed. |
/chat/summary/stream | absent | The summary failed. | Show message, or fall back to no summary. |
/spot/overview/stream | absent | The overview failed, or the ref cannot be described. The event carries the ref when it cannot be described: a poi: or region: ref that is not at the point, place: without hint.name, or a building or an address, which the API does not describe. | Show no overview. Do not retry an event that carries a ref. |
/chat/button/stream | none | This stream has no error event. A failure closes the connection without end. | Treat a closed stream with no end as failed. |
Rules for every stream:
- An
errorevent replacesend. Nothing but an optionalquotaevent follows it. - A connection that closes with neither
endnorerrorhas failed. - The
quotaevent is not an error. It is a usage report sent afterendorerror. On a stream read with a session token it carries onlytype,meterandreset_at. - An unknown event name is not an error. Ignore it.
A stream that fails or is closed part-way is still charged. On /chat/control/stream the charge is never less
than an estimate of the conversation you sent, so it can be more than the hold placed at the start: see
How a stream is charged. A retry is a new,
separately charged request.
Errors a browser hides
When the origin or credential is refused, the response has no CORS header, and the browser keeps the status
and body from your code. The page sees a CORS error in the console and a rejected fetch; the SDK logs an
error that contains [kaleidr:<product>].
To read the real code, open the Network tab and select the failed request. The common ones are
origin_not_allowed, Invalid or missing API key and server_key_in_browser. See
CORS and allowed origins.
A 4xx with no Kaleidr JSON body at all comes from the network edge, in front of the API. It is either rate limiting (back off and retry later) or a request whose declared size is far above any endpoint's limit (send less; retrying the same request gets the same answer).
Tile host
Requests to tile.kaleidr.com that do not come from a Kaleidr embed with a valid publishable key on an
allowed origin get:
| Status | Code | Retry-After | What to do |
|---|---|---|---|
| 429 | tile_access_restricted (in error, with message and docs) | 3600 | Use the Tile product with a publishable key, and add the page's origin to the key if it has an allowed-origins list. |
Key management
Errors from https://api.kaleidr.com/shared-api/api-keys, which the API Keys page uses and which you can call
with your account's access token. All use the wrapped shape.
| Status | Code | When it happens | What to do |
|---|---|---|---|
| 401 | varies | No valid account access token. A platform key is not accepted here. | Sign in again. |
| 403 | org_admin_required | Your account is not an owner or admin of the organization. | Ask an owner or admin. |
| 403 | plan_required | The organization has no plan. | Choose a plan at kaleidr.com/billing. |
| 403 | free_plan_publishable_only | A server key was requested on the Free plan. Also returned when a server or legacy key is rotated on Free. | Create a publishable key, or move to Pro. |
| 403 | tier_capability_not_allowed | Creating a key: scopes or allowed_products names something your plan does not admit. In practice this is a request for the vendor scope on Free or Pro; it is refused whether or not vendor_acknowledged is sent, and no key is created. Editing a key: allowed_products names a product the plan does not include, or, on Free, is set to an empty list. | vendor needs Enterprise. Otherwise name scopes and products the plan admits. |
| 404 | key_not_found | No key with that id in this organization. Edit and revoke match a key in any status, so revoking a key that is already revoked returns 200. Rotate needs an active key, and returns this code for a revoked one. | Check the id. For a rotate, the key may be revoked. |
| 409 | key_limit_reached | The organization holds its plan's limit of active keys (Free 2, Pro 10, Enterprise 25). | Revoke a key, or rotate instead of creating. |
| 409 | key_already_rotated | The key has been rotated, whether or not its grace period has ended. It cannot be rotated again, and an edit that clears expires_at or sets it later than the end of the grace period is refused. An earlier expires_at and the key's other fields can still be edited. | Use the replacement key. |
| 409 | key_expired | A key that was never rotated has passed its expiry, and the edit includes expires_at. An expired key cannot be given a new expiry; its other fields can still be edited. | Create a new key. |
| 413 | request_body_too_large | The request body is larger than 1 MiB. | Send a smaller body. |
| 422 | unknown_scope | scopes contains a value other than ai, maps, design, vendor. | Fix the list. |
| 422 | unknown_product | allowed_products contains a value other than chat, editor, viewer, tile. | Fix the list. |
| 422 | vendor_scope_requires_acknowledgement | On Enterprise, a publishable key with the vendor scope was requested without vendor_acknowledged: true. On Free and Pro the same request is a 403 tier_capability_not_allowed. | Read the vendor scope, then send the field. |
| 422 | no_fields_to_update | An edit request with no editable field. | Send at least one field. |
| 422 | validation list | A malformed origin, IP entry, expiry in the past, or name. | Fix the field named in loc. |
| 500 | insert_failed, session_unavailable | A platform fault. | Retry. |
| 503 | api_key_pepper_not_configured | Key creation is unavailable because of a platform fault. | Retry; contact support if it persists. |
Business-data upload
Errors from the upload routes under https://api.kaleidr.com/shared-api/datahub, and from the mapping-proposal
call. The complete list, with the file and field limits behind each 422, is in
Upload your business data. The ones that refuse a request
before a file is read, and the ones from the calls that unpublish and delete a release:
| Status | Code | When it happens | What to do |
|---|---|---|---|
| 403 | org_admin_required | The account is not an owner or admin of the organization. Also returned by the mapping-proposal call. | Ask an owner or admin. |
| 403 | vendor_data_not_on_plan | The organization's plan may not upload business data. Uploading it is part of Enterprise only. Returned by the upload, publish, re-dispatch and mapping-proposal calls. Never returned by the calls that read, unpublish or delete. | Move to Enterprise. GET /shared-api/datahub/limits reports whether an organization may upload, on every plan, without refusing. On Free and Pro, add data to a map in Kaleidr Studio instead: see Datasets on a map. |
| 404 | release_not_found | Unpublish or delete: no release with that id in the organization, including one whose deletion has completed. | For a delete, treat it as already done. |
| 409 | org_ingest_in_progress | Another upload of the organization is being processed. Returned by the upload, publish and re-dispatch calls. | Wait for it to finish, then send again. |
| 409 | release_not_published | Unpublish was called on a draft or failed release. | Nothing to unpublish. Delete it instead. |
| 409 | release_is_published | Delete was called on the published release. | Unpublish it, then delete it. |
| 413 | request_body_too_large | The upload request is larger than 32 MiB. | Send a smaller file. The file itself may be up to your organization's size limit. |
| 422 | { "errors": [ … ] } in detail | The file is over a row or size limit, a field of a JSON bundle is over its length limit, or the JSON is nested too deeply. A reason for a limit set in your agreement names the limit and the plan. | Read each reason and fix the file. |
| 429 | ingest_rate_limited | The organization started 30 uploads in the last hour. | Wait, then send again. |
| 429 | a sentence in detail | The account made 30 mapping proposals in the last hour. The body has no error field. | Wait, then send again. |
| 429 | quota_exceeded in error | Mapping proposal: the organization's AI credits are used up. The body is the standard quota body. | Wait until reset_at, add credits, or write the mapping by hand. |
Dataset uploads in Kaleidr Studio
This section is not part of the key-authenticated platform API. It describes what Kaleidr Studio receives when a signed-in user adds a dataset to a map in the Data panel and the upload is refused for its size. No API key is involved. There are two refusals, and they mean different things:
| Status | Code | When it happens | What to do |
|---|---|---|---|
| 402 | quota_exceeded | A new dataset upload is over one of the organization's plan limits: the rows in one dataset, the size of one file, or the rows across all of the organization's datasets. The limit_key field says which. Data already stored is kept. Studio shows the message with a link to upgrade. | Upload a smaller file, or remove datasets you no longer need. On Free, move to Pro. On Pro or Enterprise, contact Kaleidr for a higher total across the organization; the rows in one dataset and the size of one file cannot be raised past the ceilings in the next row. |
| 400 | VALIDATION_ERROR | The file or the dataset is over a ceiling that applies on every plan: a file larger than 64 MiB, a dataset of more than 100,000 rows, a geographic file of more than 50,000 features, or an image overlay over 16 MiB. The code is in error. A plan change does not admit the upload. | Split the file into smaller ones. Do not retry the same file, and do not offer an upgrade. |
Which one you get depends on the size, not on the plan alone. The ceiling is checked first, so a file over 64 MiB or a dataset over 100,000 rows is a 400 on Free, Pro and Enterprise alike. The 402 is for an upload that is within the ceiling and over the organization's own limit: for example a 40 MiB file on Free, where the limit is 32 MiB.
402 quota_exceeded
The body is a problem-details object that also carries Studio's own error and message fields. The code is
in both error and code, and the sentence is in both message and detail:
{
"error": "quota_exceeded",
"message": "Your plan allows 250,000 rows across all of your organization's datasets. Delete a dataset or upgrade to add more.",
"type": "https://errors.kaleidr.com/datasets/quota_exceeded",
"title": "Quota Exceeded",
"status": 402,
"detail": "Your plan allows 250,000 rows across all of your organization's datasets. Delete a dataset or upgrade to add more.",
"code": "quota_exceeded",
"limit_key": "studio.data.rows.max",
"limit": 250000,
"used": 248500,
"tier": "free",
"upgrade_url": "https://kaleidr.com/pricing"
}
| Field | Meaning |
|---|---|
error, code | Always quota_exceeded. The same value twice. |
message, detail | The same sentence twice. It names the limit in plain words and is written to be shown to a person. |
type | Always https://errors.kaleidr.com/datasets/quota_exceeded. |
title | Always Quota Exceeded. |
status | Always 402. |
limit_key | Which limit refused the upload. See the next table. |
limit | The organization's limit, in the unit of that limit. |
used | For the total, the rows the organization already holds. For the other two, the rows or the MiB of this upload. |
tier | The organization's plan: free, pro or enterprise. It is unknown when the plan could not be read; the Free limits then apply. |
upgrade_url | Where to change the plan. |
limit_key | Limit | Unit of limit and used | The sentence |
|---|---|---|---|
studio.dataset.rows.max | Rows in one dataset | Rows | "Your plan allows 50,000 rows in one dataset. Upload a smaller file or upgrade." |
studio.dataset.file.mb | Size of one uploaded file | MiB | "Your plan allows files up to 32 MB. Upload a smaller file or upgrade." |
studio.data.rows.max | Rows across all of the organization's datasets | Rows | "Your plan allows 250,000 rows across all of your organization's datasets. Delete a dataset or upgrade to add more." |
The numbers in the sentences are the Free limits; the sentence you receive carries your organization's own.
Show the sentence to a person and branch on code and limit_key. This 402 is a different response from the
429 quota_exceeded of the platform API, which is about AI credits.
400 VALIDATION_ERROR
The body has two fields and no problem-details members:
{
"error": "VALIDATION_ERROR",
"message": "File is larger than 64 MB."
}
| Refused for | message |
|---|---|
| A file over 64 MiB | "File is larger than 64 MB." |
| A dataset of more than 100,000 rows | "Maximum 100000 records per dataset" |
| A geographic file of more than 50,000 features | "<filename> has more than 50000 features. Split it or simplify the geometry before uploading." |
| An image overlay over 16 MiB | "Overlay image is larger than 16 MB." |
The same status and code are returned for other problems with an uploaded file, such as a file type Studio
does not read or a file with no usable rows; message says which. Treat every 400 from an upload as "this
file cannot be added as it is", and show message.
Related
- Auth and scopes: credentials and the session exchange.
- Quota and rate limits: the 429 bodies in detail.
- SSE wire contract: events on a stream.
- Troubleshooting: problems by symptom.