Pular para o conteúdo principal
This page is not available in Português yet. You are reading the English version.

Server quickstart

By the end of this page your backend sends a question to Kaleidr and receives a streamed answer: the text as it is written, plus every place it mentions as structured data with coordinates. You will make the call with curl, then with a short script, and you will know what each refusal means.

This needs a server key, which is available on the Pro and Enterprise plans. On the Free plan, use a publishable key in a web page instead: see Choose your path.

There is no Kaleidr package to install for server use. The API is plain HTTPS with JSON bodies, and the answer is a Server-Sent Events stream that any HTTP client can read.

Before you start​

  • A Kaleidr organization on the Pro or Enterprise plan, and an account that is its owner or an admin.
  • A terminal with curl, and Node 18 or later for the script.
  • A place to keep a secret: an environment variable or your secret manager. A server key must never be in a web page, a mobile app or a repository.

Step 1: Create a server key​

  1. Sign in and open kaleidr.com/api-keys.
  2. Create a key. Give it a name, and choose the Server key type. (On the Free plan this option is not offered.)
  3. Copy the key. It starts with kld_sk_live_ and is shown once.

Put it in an environment variable.

export KALEIDR_KEY="kld_sk_live_…"

On Windows PowerShell:

$env:KALEIDR_KEY = "kld_sk_live_…"

You should see the new key in the list on the API Keys page, with the type Server. A key created there carries the ai, maps and design scopes; this page needs ai.

Step 2: Make the first call​

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?"}]
}'

-N matters: it stops curl from buffering, so you see the answer arrive.

You should see lines begin to print within a few seconds and continue until the answer is complete (values here are illustrative, and the output is shortened):

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: 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": [ … ], "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"}

If you get one line of JSON and the command exits, the request was refused. Go to If it does not work.

Step 3: Read the answer​

The stream is a sequence of frames separated by blank lines. Three kinds matter first:

FrameHow to recognize itWhat it gives you
ProseA data: line with no event: line before ittext: the next piece of the answer. Append the pieces in order.
endevent: endfull_text: the complete answer. places and actions: the final structured result.
errorevent: errormessage: the answer failed. There will be no end.

Lines that start with a colon, such as : keep-alive, are heartbeats. Skip them.

The text can contain double-bracket markers around place names, such as [[Louvre Museum]]. They mark where the answer refers to a place. Remove the brackets when you show the text.

Step 4: Act on place events​

Each place the answer mentions arrives as a place event while the text is still streaming:

{ "name": "Louvre Museum", "coordinates": { "lat": 48.8606, "lng": 2.3376 }, "category": "museum" }
FieldUse
nameThe place's name, and the key to match it with later events for the same place.
coordinateslat and lng in decimal degrees. Put a marker here.
address, categoryOptional. Show them on the marker or in a list.
sourceOptional. "vendor" means the place came from your organization's own data.

Two rules keep a map correct:

  • A place can arrive without coordinates, marked unverified: true, when the answer names it but it could not be located. List it if you like; do not draw a pin.
  • The same name can arrive again with more detail. Keep one record per name, and do not move a pin once it has coordinates.

When end arrives, its places array is the final list. Merge it over what you collected, by name, and remove any name listed in end.dropped_places.

Other events (early_actions, route, prompt_options and more) are described in the SSE wire contract. Your client must ignore any event name it does not handle: new ones are added without notice.

Step 5: Continue the conversation​

Kaleidr does not keep the transcript for you. To ask a follow-up, send the earlier turns again and repeat the same session_id:

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?"},
{"role": "assistant", "content": "The Louvre Museum is on the right bank of the Seine, with the Tuileries Garden directly to its west."},
{"role": "user", "content": "How do I walk from one to the other?"}
]
}'

You should see an answer that understands "one" and "the other". A directions question usually adds route_clear and route events before the text, carrying the line to draw.

About session_id:

  • Use one id per conversation of one end user, generated by you: a UUID is fine. The server keeps that conversation's short-term context under it. An id is 1 to 128 characters from letters, digits, _, ., : and -; anything else is refused with 422 invalid_session_id.
  • Never share an id between your users. Their context would mix.
  • Ids are kept per organization. Another organization that sends the same id does not see or change your conversation.
  • If you leave it out, the server generates one for that request and returns it in the phase and start events. Without a stable id, each request is treated as a new conversation.

About the size of messages:

  • A request can carry at most 400 messages, 64,000 characters in one message and 200,000 characters in all messages together. Over a limit, the request is refused with 422 and a code: too_many_messages, message_too_long or conversation_too_long.
  • You pay for what you send. Every turn you resend is read again as input. Trim old turns once a conversation grows: keep the most recent ones and drop the rest. Reading the stream shows how.

Only user and assistant messages are read. A system message is ignored, so you cannot use one to give instructions or a persona. You can also send location (the lat and lng the user is looking at), map_zoom and lang: see Endpoints.

Step 6: Handle refusals​

A refused request is not a stream. It is a normal HTTP response with a JSON body. Check the status before you read frames.

401: the key is not accepted.

{ "detail": "Invalid or missing API key" }

Check that the variable is set and the key is not revoked. Do not retry.

403: the key is known, and the answer is no.

{ "detail": "insufficient_scope" }
detailCause
insufficient_scopeThe key was created without the ai scope.
tier_capability_not_allowedYour plan no longer includes what the key was created for.
product_not_allowedThe key is limited to products that do not include Chat.
server_key_in_browserThe request carried an Origin header. A server key is refused from a browser.
ip_not_allowedThe key has an IP allowlist and this server is not in it.

Do not retry any of these.

429: quota or rate. The body says which.

{ "error": "rate_limited", "detail": "API key rate limit exceeded. Slow down." }
errorRetry?
rate_limited, org_overloaded, b2b_overloaded, limiter_unavailableYes, after the number of seconds in the Retry-After header.
quota_exceededNo. Your organization's AI credits, or this key's cap, are used up. The body's reset_at says when the period ends.

Every code is listed in Errors.

The complete script​

Save this as quickstart.mjs. It makes the call, prints the answer as it streams, prints each place, and reports a refusal.

// quickstart.mjs: Node 18 or later, no dependencies.
// Run with: node quickstart.mjs "Where is the Louvre, and what is next to it?"
import { randomUUID } from 'node:crypto';

const question = process.argv[2] ?? 'Where is the Louvre, and what is next to it?';

const res = await fetch('https://api.kaleidr.com/inference-api/b2b/v1/chat/control/stream', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.KALEIDR_KEY}`,
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify({
session_id: randomUUID(),
messages: [{ role: 'user', content: question }],
}),
});

if (!res.ok) {
// A refusal is JSON, not a stream.
console.error(`Refused: HTTP ${res.status}`, await res.text());
process.exit(1);
}

const decoder = new TextDecoder();
let buffer = '';
let event = null; // the name on the most recent "event:" line, or null for prose
let finished = false;

for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // the last piece may be half a line: keep it for the next chunk

for (const line of lines) {
if (line.startsWith('event:')) {
event = line.slice(6).trim();
} else if (line.startsWith('data:')) {
const data = JSON.parse(line.slice(5));
if (event === null && typeof data.text === 'string') {
process.stdout.write(data.text);
} else if (event === 'place' && data.coordinates) {
console.log(`\n [pin] ${data.name} (${data.coordinates.lat}, ${data.coordinates.lng})`);
} else if (event === 'end') {
finished = true;
console.log(`\n\nDone: ${data.places.length} places, ${data.total_tokens ?? '?'} tokens.`);
} else if (event === 'error') {
finished = true;
console.error(`\n\nThe answer failed: ${data.message}`);
} else if (event === 'quota') {
console.log(`AI credits used this period: ${data.used} of ${data.limit}.`);
}
// Any other event name is ignored on purpose.
} else if (line === '') {
event = null; // a blank line ends the frame
}
}
}

if (!finished) console.error('\n\nThe stream closed before the answer finished.');
node quickstart.mjs "Three bakeries near the Louvre"

You should see the answer print word by word, a [pin] line for each located place, then the totals.

This script is deliberately small. It has no timeouts and no retries. For production code, use the clients in Reading the stream.

How it behaves​

  • The answer takes time to start. A hard question can take tens of seconds before the first text. The phase event tells you work has begun, and a : keep-alive comment arrives every 10 seconds of silence so the connection is not dropped.
  • Structured data arrives beside the text, not inside it. Places, map actions and routes are their own events. You can ignore the prose and drive a map from the events alone.
  • end is the authoritative result. What streamed before it is for showing progress.
  • quota comes last. After end, one more event reports your organization's AI-credit usage. Keep reading until the connection closes.
  • Each call is charged from your organization's AI credits, by the tokens the answer used, input and output. A stream you abort is still charged: at least for the conversation you sent, however little of the answer you read. See Quota and rate limits.

Check that it worked​

  • The response status is 200 and its Content-Type is text/event-stream.
  • The first event is phase, and its session_id is the one you sent.
  • Text arrives in several pieces, not all at once at the end.
  • There is exactly one end event, and its full_text holds the complete answer.
  • Your usage on kaleidr.com/billing has gone up.

If it does not work​

What you seeCauseFix
401 {"detail":"Invalid or missing API key"}The header is empty or the key is wrong, revoked or expired.Print the variable's length to confirm it is set. Check the key on the API Keys page.
401 {"detail":"publishable_requires_session"}You sent a publishable key (kld_pk_…).Use the server key.
403 {"detail":"server_key_in_browser"}The request has an Origin header: it was sent from a browser, or a tool added the header.Call from your backend, and do not set Origin.
403 {"detail":"insufficient_scope"}The key lacks the ai scope.Create a key that has it.
403 {"detail":"ip_not_allowed"}The key has an IP allowlist that does not include this machine.Call from an allowed address or change the list.
429 with "error":"quota_exceeded"AI credits are used up.See the body's reset_at; add credits or wait.
429 with "error":"rate_limited"Too many requests with this key in the last minute.Wait for Retry-After, then retry.
404 {"detail":"Not Found"}The URL is wrong.The path is /inference-api/b2b/v1/chat/control/stream.
422 with a detail arrayThe JSON body is malformed, or a field has the wrong type.Read loc and msg in the body.
422 {"detail":"conversation_too_long"}, or too_many_messages, message_too_longThe request carries more than 200,000 characters in total, more than 400 messages, or one message over 64,000 characters.Drop the oldest turns and send again.
422 {"detail":"invalid_session_id"}session_id is longer than 128 characters or has a character outside letters, digits, _, ., :, -.Use a UUID.
413 {"detail":"request_too_large"}The request body is larger than 2 MiB.Send fewer or shorter messages.
403 {"detail":"product_not_allowed"}The key's allowed products do not include chat.Add chat to the key's allowed_products, or use another key.
Nothing prints for a long time, then everything at onceOutput is buffered.Add -N to curl. In your own stack, turn off response buffering in any proxy in front of your client.
The stream stops with no endA timeout in your HTTP client or a proxy closed the connection.Set the idle timeout to 30 seconds or more, and remove any short total timeout.
No Server option when creating a keyThe organization is on the Free plan.Server keys need Pro or Enterprise.

Limits​

  • REST only. There is no server-side SDK and no npm or PyPI package. The browser SDK, kaleidr.js, is for web pages.
  • No instructions, persona or tools. The request carries a conversation, a location and a language. system messages are ignored.
  • No stored transcripts. You keep the conversation and resend it.
  • One rate limit per key. All calls made with a key share its requests per minute.
  • Mobile apps call Kaleidr through your backend with this server key. Do not ship a server key inside an app. See the security model.

Next steps​