Skip to main content

Quota & rate limits

Kaleidr limits usage in two different ways, and both answer with HTTP 429:

  • Quota is how much your organization may use in a billing period. Running out means "wait for the period to reset, or add more".
  • Rate limits are how fast you may call. Hitting one means "slow down for a few seconds".

The response body tells you which one you hit. This page shows every body, what each call is charged, and how to handle each case.

The two pools​

Usage is counted per organization, in two pools.

PoolWhat spends itFreeProEnterprise
AI creditsChat answers, place lookups, address search, design analysis500,000 per period5,000,000 per periodCustom, set in your contract
Map loadsEach Tile session, and each view of a published or embedded map50,000 per period50,000 per periodThe same base allowance; higher volumes are set in your contract

Three things about the pools are easy to get wrong:

  • AI credits are one pool for the whole organization. Your API keys draw from the same AI credits your team uses inside Kaleidr's own apps. There is no separate API budget, and every key shares it.
  • The period is your billing period. For a paid subscription that is the subscription's monthly cycle, starting on the day you subscribed. Without one it is the calendar month. Periods roll over at midnight US Eastern time.
  • Packs raise the limit. Purchased AI-credit packs and map-load packs are added to the plan's allowance, and the limit you see in responses includes them.

Plan changes reach your existing keys within a few minutes. You do not need to create new keys.

What each call is charged​

CallPoolCharge
POST /chat/control/stream, /chat/summary/stream, /chat/button/stream, /spot/overview/streamAI creditsThe tokens the answer used, input plus output. See How a stream is charged.
POST /chat/control/routenoneNot charged. A hold is placed while the route is planned and always released.
POST /spotAI creditsA flat fee per call, charged whether or not anything was found: the fee of /retrieval/poi/enrich when want includes content or photos and the content is fetched for this call, otherwise the fee of /retrieval/geocode/forward when it includes address. Content already fetched for an earlier request costs nothing. A call for places, regions and outlines only is free.
GET /retrieval/poi/enrichAI creditsA flat fee per call, charged whether or not content was found.
GET /retrieval/geocode/forwardAI creditsA smaller flat fee per call, charged even when there are no results. Not charged if the lookup itself fails.
POST /design/analyzeAI creditsA flat fee per successful call. Not charged when the call fails.
POST /design/apply-direct, GET /design/styles, /design/samples, /design/themesnoneNot charged.
POST /sdk/sessions for chat, editor, viewernoneNot charged.
POST /sdk/sessions for tileMap loadsOne map load per exchange.
POST /sdk/share-sessionsnoneNot charged. The chat calls made with the session are charged to the map owner.
POST /inference-api/vendor/mapping/propose, the mapping proposal of the business-data upload APIAI creditsThe tokens the proposal used. Called with an account sign-in, not an API key. When the credits are used up it answers 429 quota_exceeded; it also has its own limit of 30 proposals per account per hour, and the two 429s differ in their body. Uploading, publishing, unpublishing and deleting are not charged.

Your current usage and limits are on kaleidr.com/billing.

How a stream is charged​

A streaming call does not know its cost until it finishes, so it is charged in two steps:

  1. Hold. When the request is admitted, a fixed number of credits is held from the pool. If the pool cannot cover the hold, the request is refused with quota_exceeded before any answer is produced. This means a call needs some headroom in the pool to start, even when it would have cost less.
  2. Settle. When the stream ends, the hold is replaced by the real charge: the tokens the answer used. The real charge can be more or less than the hold.

If you abort a stream, or it fails before the answer completes, the token count is not known and the charge is an estimate:

StreamCharge when it ends without completing
/chat/control/streamWhichever is larger: an estimate of the conversation you sent (the user and assistant messages in the request), or an estimate of the text already streamed, which is counted up to the hold. This can be more than the hold.
/chat/summary/stream, /chat/button/stream, /spot/overview/streamAn estimate of the text already streamed, never more than the hold.

So closing a chat stream early does not make a long conversation free: the request is charged for at least its own size, however little of the answer you read. Keep requests small by sending only the turns the next answer needs. See Reading the stream.

A limit reached while a stream is running does not interrupt that stream. The check happens at admission; the next request is the one that is refused.

A refused request is not charged. A request that is refused before it is processed (a rate or concurrency limit, a validation error, a body over the size limit) holds no AI credits and no concurrency slot afterward. You can retry it without having paid for the refusal.

A cap on one key​

A key can carry a monthly cap (monthly_cap_tokens, set through the key management API). It limits what that one key may spend from the organization's AI credits in a period. The organization's pool is still the outer limit.

A request that would take a capped key over its cap is refused with the same quota_exceeded body, where limit and used are the key's numbers instead of the pool's.

Quota exceeded​

HTTP/1.1 429 Too Many Requests
Retry-After: 1296000
Content-Type: application/json
{
"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
}
FieldMeaning
errorAlways quota_exceeded for this case. Branch on this field.
detailA sentence for a person. Do not parse it.
product, meterIdentify the API's AI-credit counter. Always b2b and b2b.tokens for platform keys.
tierYour organization's plan.
limit, used, remainingThe pool's numbers in credits, or the key's numbers when a per-key cap refused the call. remaining can be more than zero: the call was refused because the hold did not fit.
reset_atWhen the period ends, as an ISO 8601 timestamp with offset. Use this to tell a user when service resumes.
periodmonth.
reset_in_secondsSeconds until reset_at, the end of the period your pool resets on (your subscription cycle, or the calendar month without one). The Retry-After header carries the same number.

The body is flat: the fields sit at the top level, not inside a detail object.

With a session token​

The body above is what a server key or legacy key receives. A request made with a session token runs in a visitor's browser, so its refusal does not disclose the organization's plan, usage or limit. This applies to a session exchanged from a publishable key and to a published map's visitor session.

{
"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
}
FieldServer key or legacy keySession token
error, detailYesYes
meterYesYes
reset_at, reset_in_secondsYesYes
product, periodYesNo
tierYesNo
limit, used, remainingYesNo

The status, the Retry-After header and the meaning are the same. In browser code, branch on error and show reset_at; do not read limit, used or remaining. To show usage numbers, read them on your backend with a server key, or on kaleidr.com/billing.

If the body shows used: 0 while you know the organization has been active, the API could not read your usage and refused the call to be safe. Retry after a few seconds instead of waiting for reset_at.

Rate limits and concurrency​

Three limits sit in front of every runtime call. Each refuses the excess at once; nothing is queued.

LimitCounted perWhat it covers
Requests per minuteKeyA rolling 60-second window over every runtime call made with the key and with every session exchanged from it. All visitors of a site that uses one publishable key share this budget.
Session exchanges per minuteKeyCalls to POST /sdk/sessions. A separate budget, so mounting embeds does not use up runtime calls.
Concurrent requestsOrganizationCalls in progress at the same moment across all of the organization's keys. A stream counts for as long as it is open.

A key's requests-per-minute value is not editable from the API Keys page or the key management API. If your traffic needs a higher rate, contact us.

Every one of these refusals has the same shape and the same header:

HTTP/1.1 429 Too Many Requests
Retry-After: 5
Content-Type: application/json
{ "error": "rate_limited", "detail": "API key rate limit exceeded. Slow down." }
errorMeaningWhat to do
rate_limitedThe key's requests-per-minute or exchange budget is used up.Wait Retry-After seconds, then retry. Add jitter.
org_overloadedYour organization has too many calls in progress.Wait for one of your open calls to finish, then retry. Close streams you no longer need.
b2b_overloadedThe API is at capacity for all callers.Wait Retry-After seconds, then retry with backoff.
limiter_unavailableThe rate limiter could not answer.Retry shortly. Slowing down does not help; this is not about your rate.
visitor_limit_reachedOne visitor of a published map's chat has asked too much, per minute or per day.Show the visitor a "try again later" message. See Chat on a published map.

Requests are also rate limited at the network edge, per IP address and per credential. A request blocked there does not carry one of the JSON bodies on this page. Treat a 4xx response without a Kaleidr body as "back off and retry later".

Map loads​

A map load is counted each time a Kaleidr map starts up for a viewer:

  • each session exchange for product: "tile", including the SDK's refresh and each remount of the element;
  • each view of a published or embedded map.

The embedded Editor's session exchange is not a map load. Its basemap requests that carry your publishable key (the style, the sprites and the fonts) are counted in bulk instead: every 20 of them your organization makes within an hour add one map load, after the fact. They are never refused for the allowance. Map tile data itself is not counted.

Every plan stops at its allowance. When the pool is used up, the next load is refused and not counted. There is no grace band on paid plans; a purchased map-load pack raises the limit immediately.

From the session exchange the refusal is:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "detail": "map_load_quota_exceeded" }

This body is wrapped in detail, unlike the flat quota body above, and has no Retry-After header. Retrying does not help until the period resets or you add a pack. In the page, the Tile embed fails to start and the SDK logs kaleidr session exchange failed (429).

Direct tile requests​

The basemap tile host is not a public tile API. The supported way to show a Kaleidr basemap is the Tile product. A request to tile.kaleidr.com that does not come from a Kaleidr embed with a valid publishable key on an allowed origin is refused:

HTTP/1.1 429 Too Many Requests
Retry-After: 3600
Cache-Control: no-store
Content-Type: application/json
{
"error": "tile_access_restricted",
"message": "Direct access to tile.kaleidr.com requires a Kaleidr publishable key. Mint a free one at kaleidr.com/api-keys and pass it as ?key= from an allowed origin. Kaleidr-hosted maps and embeds are unaffected.",
"docs": "https://docs.kaleidr.com/get-an-api-key"
}

This response comes from the tile host, not from the platform API, so it uses message where the API uses detail. A new or edited key can take a few minutes to be accepted for tiles.

The quota event on a stream​

A chat stream reports the pool after it has been charged. The quota event is the last event of the stream, sent after end or error:

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"}

That is the event on a stream read with a server key or legacy key. On a stream read with a session token (exchanged from a publishable key, or a published map's visitor session) the event carries no plan and no numbers:

event: quota
data: {"type": "quota", "meter": "b2b.tokens", "reset_at": "2026-11-01T04:00:00+00:00"}
FieldServer key or legacy keySession token
type, meter, reset_atYesYes
product, periodYesNo
tierYesNo
used, limitYesNo

With a server key, use it to update a usage meter without a second request. Three limits apply:

  • It has no remaining field. Compute limit - used.
  • It is sent on a best-effort basis. A stream can end without it, so do not wait for it.
  • It reports; it does not warn. It never arrives in the middle of an answer, and it cannot stop a stream.

To receive it, keep reading until the response body closes. A client that stops at end misses it.

Handling 429 in code​

Read the body before deciding. The status alone does not tell you whether to retry.

You receiveRetry?When
error: "quota_exceeded"NoAfter reset_at, or after adding credits or raising the key's cap. Show the user a "limit reached" message.
error: "rate_limited", "org_overloaded", "b2b_overloaded", "limiter_unavailable"YesAfter Retry-After seconds, with jitter, a few times at most.
error: "visitor_limit_reached"NoLater; tell the visitor.
detail: "map_load_quota_exceeded"NoAfter the period resets or a map-load pack is added.
error: "tile_access_restricted"NoFix the embed: use the Tile product with a publishable key on an allowed origin.
const RETRYABLE = new Set(['rate_limited', 'org_overloaded', 'b2b_overloaded', 'limiter_unavailable']);

async function callKaleidr(url, init, attempt = 0) {
const res = await fetch(url, init);
if (res.status !== 429) return res;

const body = await res.json().catch(() => ({}));
if (RETRYABLE.has(body.error) && attempt < 3) {
const seconds = Number(res.headers.get('retry-after')) || 5;
await new Promise((resolve) => setTimeout(resolve, (seconds + Math.random()) * 1000));
return callKaleidr(url, init, attempt + 1);
}

const error = new Error(body.error || body.detail || 'rate limited');
error.status = 429;
error.code = body.error || body.detail;
error.resetAt = body.reset_at; // present for quota_exceeded
throw error;
}

In a browser, the Retry-After header is readable from JavaScript on platform API responses.