Skip to content

Sessions and session IDs

View as Markdown

A session is a durable conversation between one end user and one agent. It holds every message, in order, and it is what gives the agent its memory of the conversation. You can address a session by the ID we issue (sess_…) or by your own ID (external_id), such as an order number, a ticket or a chat thread in your system.

This contract is part of the API’s stability promise.

  1. We always mint the ID. Every session has id = sess_<26 chars>. It is unguessable and time-sortable.
  2. You may add your own external_id:
    • 1–128 characters matching ^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$ (an underscore is fine, as in cus_123);
    • case-sensitive;
    • unique within the project;
    • set once and immutable;
    • must not start with sess_.
  3. Both IDs work everywhere a session appears in a URL. Responses always return both.
  4. Get-or-create. POST /v1/sessions with an external_id:
    • returns 201 (created: true) or 200 (created: false);
    • if_exists: "error" makes a match return 409 session_exists;
    • the same external_id with a different agent returns 409 session_agent_mismatch;
    • closed sessions reopen unless if_closed: "error";
    • concurrent first calls are safe (a unique index plus an upsert).
  5. IDs are not credentials.
    • Every call is authenticated.
    • Client tokens can only reach sessions of their own end user.
    • Browsers never address sessions by external_id with a publishable key alone.
  6. No personal data in IDs. We return warnings: ["external_id_looks_like_phone"] (or …_email) when an ID looks like contact details. Hash such values on your server first (see below), and keep contact details in the end user’s traits instead.
  7. Memory and limits follow the end user, not the session ID. Rotating session IDs does not reset the action ledger or per-user limits for a verified end user.
  8. History spans the whole session (the last history_limit messages). Idle gaps only refresh the frozen prompt and agent version (segments) and are independent of billing windows.
  • Only three things create sessions: POST /v1/sessions, POST /v1/widget/sessions and the OpenAI-compatible endpoint when you name a session. Any other URL with an unknown {session} returns 404 session_not_found.
  • Defaults: if_exists defaults to "resume" and if_closed to "reopen".
  • input on POST /v1/sessions is always appended and answered, whether the session was just created or resumed. One call per turn is a complete integration.
  • A session’s end user is fixed at creation. Resuming with a different end_user.external_id returns 409 session_end_user_mismatch. Other creation-only fields are ignored on resume and the response says so with warnings: ["create_params_ignored"].
  • How a path is read: {session} is treated as our ID only when it is sess_ followed by 26 lowercase base32 characters. Anything else is looked up as an external_id.
  • The playground is separate. Test sessions from the dashboard live in their own external_id namespace and never resolve to, or write into, a real session.
نافذة الطرفية
curl https://api.k-agent.kerneltics.com/v1/sessions \
-H "Authorization: Bearer $KAGENT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"agent": "store-assistant",
"external_id": "order-8812",
"end_user": { "external_id": "cus_1042", "name": "Fahad" },
"metadata": { "source": "order_page" },
"input": "Do you deliver to Abha?"
}'

The first call returns 201:

{
"session": {
"id": "sess_01k6rz4p7h2c9m5x8w3t6v1qbg",
"object": "session",
"external_id": "order-8812",
"end_user_id": "eu_01k6rz3t5v7w9x1y2z4a6b8c0d",
"channel": "api",
"status": "active",
"mode": "agent",
"concurrency": "queue",
"version_policy": "latest",
"metadata": { "source": "order_page" },
"created_at": 1791271920
},
"created": true,
"message": { "id": "msg_01k6rz5b2c4d6e8f0g1h3j5k7m", "object": "message", "role": "user" },
"run": { "id": "run_01k6rz5a9d3f6g2h8j4k7m1n5p", "object": "run", "status": "completed", "outcome": "answered", "output_text": "Yes, we deliver to Abha in 3 to 5 working days, and delivery is free on orders over SAR 200." },
"warnings": []
}

Every later call with the same external_id returns 200 with "created": false, appends the new input and answers it.

Field Type Notes
agent string, required Agent ID or slug.
external_id string Your ID for the session; see the contract above. Leave it out to use only our sess_ ID.
end_user object {external_id, name?, traits?}. Secret keys may vouch for the user, which makes them verified. Fixed for the life of the session.
variables object Values for the agent’s declared variables. Secret variables are not accepted here.
metadata object Up to 16 keys; keys up to 64 characters, values up to 512. Never sent to the model.
concurrency queue · reject · interrupt What happens when a message arrives while the agent is answering.
version_policy, version latest · pinned Pin the session to one published version, or follow the latest.
if_exists resume · error Default resume.
if_closed reopen · error Default reopen.
input string or text parts Appended and answered in the same call.
overrides object Session-level overrides the agent allows, applied from the next segment. Secret keys only.
client_message_id string Deduplicates input, as on messages.
stream, background, wait_seconds Stream the reply, return at once, or wait up to wait_seconds (default 60, max 110). The status stays 201 or 200 either way; a run that is still working is returned as it is.
Situation Response
if_exists: "error" and the session exists 409 session_exists
Same external_id, different agent 409 session_agent_mismatch
Same external_id, different end_user.external_id 409 session_end_user_mismatch
if_closed: "error" and the session is closed 409 session_closed
external_id breaks the format, e.g. sess_x or order 8812 422 external_id_invalid
{
"error": {
"type": "conflict_error",
"code": "session_agent_mismatch",
"message": "This external_id already belongs to a session with a different agent.",
"param": "external_id",
"request_id": "req_01k6rz9f1j3m5p7r9t1w3y5a7c",
"doc_url": "https://k-agent.kerneltics.com/docs/en/reference/errors/#session_agent_mismatch"
}
}

If you run several agents over the same records, give each its own ID space, for example support:order-8812 and sales:order-8812.

IDs show up in logs, URLs and exports. When an external_id looks like a phone number (8 or more digits) or an email address, the response carries a warning:

{ "warnings": ["external_id_looks_like_phone"] }

If your natural key is personal data, derive the ID on your server with a keyed hash. The result is stable for the same input, reveals nothing, and fits the external_id format: base32(HMAC-SHA256(secret_you_keep, value)), lowercase, first 32 characters. K-Agent never sees or stores your secret.

import { createHmac } from 'node:crypto';
const ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
export function hashExternalId(value, secret) {
const digest = createHmac('sha256', secret).update(value, 'utf8').digest();
let out = '';
let bits = 0;
let acc = 0;
for (const byte of digest) {
acc = ((acc << 8) | byte) & 0xffff;
bits += 8;
while (bits >= 5) {
out += ALPHABET[(acc >>> (bits - 5)) & 31];
bits -= 5;
}
}
return out.slice(0, 32);
}
hashExternalId('+966501234567', process.env.ID_HASH_SECRET);

All four produce the same value: hashExternalId('+966501234567', 'my-hmac-secret') is cs6vd2wrba2hf5w2ozuvftyoq5dewzhb. Keep the secret stable — changing it changes every ID.

Two messages can arrive while the agent is still answering the first. The session’s concurrency decides what happens:

Policy Behavior Default for
queue Messages wait their turn and are answered in order. At most 10 can wait; the 11th gets 409 session_queue_full. API sessions
reject A message that arrives during a reply gets 409 session_busy with Retry-After: 2. Nothing is stored. —
interrupt The reply in progress is abandoned (superseded) and one new reply answers every unanswered message together. Widget sessions

A superseded run never executes a tool or writes output after it is replaced. Use interrupt for chat UIs where people send several short messages in a row.

  • History spans the whole session. Each reply sees the last history_limit messages (default 12), cut at a user message so an exchange is never split. Coming back the next day does not reset the conversation.
  • History is text. Earlier turns are sent as plain text; tool calls and model reasoning stay inside the run that made them.
  • Your team’s replies count. Public human_agent messages appear to the agent as assistant turns after a handoff ends; internal notes never do.
  • Segments are the parts of a session between idle gaps (idle_timeout_minutes, default 30). At the start of a segment the agent version, the prompt and the tool list are resolved and frozen until the next segment. A new segment also starts after you publish a new version, when the session’s variables change, or after 24 hours. Segments never cut history and have nothing to do with billing.
Action How
Read a session GET /v1/sessions/{session} (ID or external_id)
Change metadata, variables, overrides or concurrency, or clear flagged PATCH /v1/sessions/{session}
Close POST /v1/sessions/{session}/close. A new user message reopens it.
Delete for good DELETE /v1/sessions/{session} — removes messages, runs, events, handoffs and tickets.
List and filter GET /v1/sessions?agent=&end_user=&external_id=&status=&mode=&flagged=&channel=&created_after=&created_before=
Read the transcript GET /v1/sessions/{session}/messages
Follow live GET /v1/sessions/{session}/events — see Runs and streaming

POST /v1/sessions/{session}/messages takes input plus optional client_message_id, stream, background, wait_seconds, variables and metadata, and returns {session, message, run}:

  • client_message_id makes a message safe to resend: the same ID returns the original {session, message, run} with Idempotent-Replayed: true instead of a second answer. The same ID with different text returns 409 client_message_id_conflict.
  • Human mode: while your team has the conversation, the message is stored, run is null, and session.mode is "human".
  • background: true returns 202 at once with the run in progress; follow it with GET /v1/runs/{run} or the session’s event stream.

Your team replies through the same endpoint with role: "human_agent" — see the Handoff Desk guide.