Skip to main content
Version: Peki

Sessions API

The Sessions API puts your agent inside your own product: a chat screen in your app, a support panel in your SaaS dashboard, or a ChatGPT-style list of chats per user. Your app decides when a new chat starts; each chat is a session. Sessions show in the Inbox like conversations from any other channel, with the channel API, so your team can follow and take over.

Base URL: https://peki.ai/api Auth: Authorization: Bearer psk_… (Authentication)

How it works​

  1. Your backend starts a session for one of your users, identified by your own user id.
  2. You send that user's messages to the session.
  3. You get the agent's reply by polling the messages, or by streaming it as it's written.

Sessions always talk to the agent's published version. Changes in the draft reach the API only after you publish.

Endpoints​

MethodPathAccessWhat it does
POST/agents/{agentId}/sessionsRead and writeStart a session
GET/agents/{agentId}/sessionsRead onlyList sessions
GET/sessions/{id}Read onlyGet a session
PATCH/sessions/{id}Read and writeRename a session
GET/sessions/{id}/messagesRead onlyList messages
POST/sessions/{id}/messagesRead and writeSend a message (JSON or stream)
POST/sessions/{id}/filesRead and writeUpload a file
POST/sessions/{id}/feedbackRead and writeRate the session
POST/sessions/{id}/resolveRead and writeResolve the session

To download a file from a message, use GET /files/{id}.

Objects​

Session​

{
"id": "671c9f20a1b2c3d4e5f60718",
"agentId": "66f1c2a9e4b0a1d2c3e4f567",
"channel": "api",
"status": "open",
"title": "Where is my order?",
"preview": "Order 10482 still hasn't arrived",
"customer": { "id": "user-1042", "name": "Maria Lopez" },
"createdAt": "2026-10-26T14:03:11.520Z",
"lastMessageAt": "2026-10-26T14:05:40.102Z",
"feedback": "up"
}
FieldTypeDescription
idstringThe session id.
agentIdstringThe agent this session talks to.
channelstringapi for sessions started here. Other channels (webchat, telegram) appear only when you read one of their conversations by id.
statusstringopen or resolved.
titlestringThe title you set, or the first 60 characters of the first message. Missing until there is one.
previewstringThe start of the latest customer message (up to 200 characters).
customer.idstringYour own user id, as you sent it when starting the session.
customer.namestringThe customer's name, if known.
createdAtstringWhen the session started.
lastMessageAtstringTime of the latest activity in the session.
feedbackstringThe latest rating, up or down. Missing until rated.

Message​

{
"id": "671ca0b3a1b2c3d4e5f60722",
"role": "agent",
"text": "Your order 10482 left our warehouse yesterday and should arrive on Thursday.",
"status": "done",
"createdAt": "2026-10-26T14:05:43.871Z"
}
FieldTypeDescription
idstringThe message id.
rolestringcustomer (your user), agent (the AI) or team (a person from your team who took over in the Inbox).
textstringThe message text. Empty while an agent reply is pending or when it failed.
statusstringpending (the agent is still writing), done or failed.
createdAtstringWhen the message was created.
authorstringFor team messages: the team member's name.
attachmentsarrayFiles on the message: { id, name, contentType, size }. Download them with GET /files/{id}.

Start a session​

POST /agents/{agentId}/sessions

Starts a new, empty session for one of your users. Start a new session whenever your app opens a new chat; you don't need one per message.

Body

FieldTypeRequiredRules
customer.idstringYesYour own id for this user. 1–128 characters. The same id always means the same customer in Peki.
customer.namestringNo1–120 characters. Saved the first time Peki sees this customer id; later sessions don't change it.
titlestringNo1–120 characters. Without it, the first message becomes the title.
curl -X POST https://peki.ai/api/agents/66f1c2a9e4b0a1d2c3e4f567/sessions \
-H "Authorization: Bearer $PEKI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"customer": {"id": "user-1042", "name": "Maria Lopez"}, "title": "Order question"}'
const res = await fetch(`https://peki.ai/api/agents/${agentId}/sessions`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.PEKI_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ customer: { id: 'user-1042', name: 'Maria Lopez' } }),
});
const session = await res.json(); // { id, agentId, channel: "api", status: "open", ... }

Response 201 with the session.

Errors

StatusMessageWhy
400Invalid bodycustomer.id is missing or a field is too long.
404Agent not foundThe agent id is wrong or belongs to another workspace.
409Publish the agent firstThe agent has no published version yet.
note

Customers from the API are their own customers in Peki, separate from website chat and Telegram customers, even if they're the same person.

List sessions​

GET /agents/{agentId}/sessions?customer={customerId}&limit={n}

Lists the agent's API sessions, most recent activity first. Use it to show a user their chat history.

Query parameters

ParameterRequiredDescription
customerNoYour user id. Only that user's sessions are returned. An unknown id returns an empty list.
limitNoHow many to return, 1–100. Default 50.

Only sessions started through the API are listed, not website chat or Telegram conversations, and not automation runs.

curl "https://peki.ai/api/agents/66f1c2a9e4b0a1d2c3e4f567/sessions?customer=user-1042&limit=20" \
-H "Authorization: Bearer $PEKI_API_KEY"
const url = new URL(`https://peki.ai/api/agents/${agentId}/sessions`);
url.searchParams.set('customer', 'user-1042');
const sessions = await (await fetch(url, { headers: { Authorization: `Bearer ${key}` } })).json();

Response 200 with an array of sessions.

There's no pagination cursor: you get at most the 100 most recent sessions. Keep your own list if your users have more.

Get a session​

GET /sessions/{id}
curl https://peki.ai/api/sessions/671c9f20a1b2c3d4e5f60718 \
-H "Authorization: Bearer $PEKI_API_KEY"

Response 200 with the session. 404 Session not found if it doesn't exist in your workspace.

This also works for conversation ids from other channels of your workspace, for example an id you received in an automation. You can read them, but only API sessions accept messages.

Rename a session​

PATCH /sessions/{id}
FieldTypeRequiredRules
titlestringYes1–120 characters.
curl -X PATCH https://peki.ai/api/sessions/671c9f20a1b2c3d4e5f60718 \
-H "Authorization: Bearer $PEKI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Late delivery, order 10482"}'

Response 200 with the updated session.

List messages​

GET /sessions/{id}/messages

Returns every message of the session, oldest first: your user's messages, the agent's replies and your team's replies.

curl https://peki.ai/api/sessions/671c9f20a1b2c3d4e5f60718/messages \
-H "Authorization: Bearer $PEKI_API_KEY"

Response 200:

[
{ "id": "671ca0a1a1b2c3d4e5f60720", "role": "customer", "text": "Order 10482 still hasn't arrived", "status": "done", "createdAt": "2026-10-26T14:05:40.102Z" },
{ "id": "671ca0b3a1b2c3d4e5f60722", "role": "agent", "text": "", "status": "pending", "createdAt": "2026-10-26T14:05:40.230Z" }
]

Send a message​

POST /sessions/{id}/messages

Sends your user's message to the agent. The agent starts writing its reply right away.

Body

FieldTypeRequiredRules
textstringOne of the twoUp to 4,000 characters.
attachmentsstring[]One of the twoUp to 5 file ids you uploaded to this session with Upload a file.

A message needs text, files or both. Sending to a resolved session opens it again.

There are two ways to get the reply.

Option 1: JSON and polling​

Without a special Accept header, the call returns at once with 202:

{
"message": { "id": "671ca0a1a1b2c3d4e5f60720", "role": "customer", "text": "Order 10482 still hasn't arrived", "status": "done", "createdAt": "2026-10-26T14:05:40.102Z" },
"reply": { "id": "671ca0b3a1b2c3d4e5f60722", "role": "agent", "text": "", "status": "pending", "createdAt": "2026-10-26T14:05:40.230Z" }
}

Then poll GET /sessions/{id}/messages until the message with reply.id has status done (or failed). A poll every 1–2 seconds is enough; most replies take a few seconds, longer when the agent calls tools.

reply is null when nobody from the AI will answer: a person from your team has taken over the conversation in the Inbox. Their answers arrive as team messages.

async function ask(sessionId, text) {
const headers = { Authorization: `Bearer ${process.env.PEKI_API_KEY}`, 'Content-Type': 'application/json' };
const base = 'https://peki.ai/api';
const sent = await (await fetch(`${base}/sessions/${sessionId}/messages`, {
method: 'POST', headers, body: JSON.stringify({ text }),
})).json();
if (!sent.reply) return null; // a team member has taken over
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 1500));
const messages = await (await fetch(`${base}/sessions/${sessionId}/messages`, { headers })).json();
const reply = messages.find((m) => m.id === sent.reply.id);
if (reply && reply.status !== 'pending') return reply;
}
throw new Error('No reply yet');
}

Option 2: streaming​

Send Accept: text/event-stream and the reply streams back on the same connection as server-sent events. Each event is one data: line with a JSON object:

typeFieldsWhen
acceptedmessageFirst: your user's stored message.
startThe agent started working.
toolname, state (running, ok or error), durationMsThe agent calls one of its tools. Use it to show "Checking your order…".
texttextThe reply so far. Each text event carries the whole text up to now, not just the new part: replace what you show, don't append.
messagemessageThe final, saved reply. Show this one.
donetimedOut (only when true)Last event. The connection closes.
curl -N -X POST https://peki.ai/api/sessions/671c9f20a1b2c3d4e5f60718/messages \
-H "Authorization: Bearer $PEKI_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"text": "Where is order 10482?"}'
data: {"type":"accepted","message":{"id":"671ca0a1a1b2c3d4e5f60720","role":"customer","text":"Where is order 10482?","status":"done","createdAt":"2026-10-26T14:05:40.102Z"}}

data: {"type":"start"}

data: {"type":"tool","name":"get_order_status","state":"running"}

data: {"type":"tool","name":"get_order_status","state":"ok","durationMs":412}

data: {"type":"text","text":"Your order 10482 left"}

data: {"type":"text","text":"Your order 10482 left our warehouse yesterday"}

data: {"type":"message","message":{"id":"671ca0b3a1b2c3d4e5f60722","role":"agent","text":"Your order 10482 left our warehouse yesterday and should arrive on Thursday.","status":"done","createdAt":"2026-10-26T14:05:40.230Z"}}

data: {"type":"done"}
const res = await fetch(`https://peki.ai/api/sessions/${sessionId}/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PEKI_API_KEY}`,
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify({ text: 'Where is order 10482?' }),
});
if (!res.ok) throw new Error(`Peki API ${res.status}: ${await res.text()}`);

const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const events = buffer.split('\n\n');
buffer = events.pop();
for (const e of events) {
if (!e.startsWith('data: ')) continue;
const event = JSON.parse(e.slice(6));
if (event.type === 'text') render(event.text); // the whole reply so far
if (event.type === 'message') render(event.message.text); // final text
}
}

Things to know about streaming:

  • No text events when the leak guardrail is on. If the agent's Guardrails check replies for leaks before they go out, the text can't stream: you get start, tool events and then the checked message. See Guardrails and privacy.
  • Show the message, not the last text. Guardrails and personal-data handling can change the final reply. The message event is what was saved and what your team sees in the Inbox.
  • A team takeover ends the stream right after accepted with done.
  • Two minutes at most. If the reply isn't finished after 120 seconds, you get {"type":"done","timedOut":true} and the stream closes. The reply still finishes; fetch it with List messages.
  • If your connection drops, the reply still finishes. Fetch it with List messages.
  • The agent's reasoning is never streamed.

Errors​

StatusMessage or codeWhyWhat to do
400Write a message or attach a fileEmpty text and no attachments.Send text or files.
400Invalid bodytext over 4,000 characters, more than 5 attachments or an id that isn't a file id.Shorten or fix the body.
404Session not foundWrong id.Check the id.
409Only sessions started through the API take messages hereThe id is a website chat or Telegram conversation.Start an API session.
409file_takenAn attachment id doesn't exist, is already attached to another message, belongs to another session, was uploaded by another key, or is older than 24 hours.Upload it again with the same key.
429This assistant is very busy right now. Please try again later.The workspace hit its daily limit of live replies.Retry later.
503Could not accept the message right now. Please try again.The message wasn't stored.Retry.

Upload a file​

POST /sessions/{id}/files
Content-Type: multipart/form-data

Uploads one file from your user into the session. Then send it with a message by putting the returned id in attachments. The agent reads it with the message.

Form fields: exactly one part, named file. No other fields.

Accepted files

KindTypes
PhotosJPEG, PNG, WebP, GIF
DocumentsPDF, TXT, Markdown, CSV
  • Up to 10 MB per file, 5 files per message and 30 files per conversation.
  • The type is checked from the file's content, not its name. SVG and HTML are never accepted.
  • Upload and send with the same API key. A file uploaded by one key can't be sent by another.
  • A file that isn't sent with a message within 24 hours is deleted.
curl -X POST https://peki.ai/api/sessions/671c9f20a1b2c3d4e5f60718/files \
-H "Authorization: Bearer $PEKI_API_KEY" \
-F "[email protected]"
const form = new FormData();
form.append('file', new Blob([bytes], { type: 'image/jpeg' }), 'damaged-box.jpg');
const file = await (await fetch(`https://peki.ai/api/sessions/${sessionId}/files`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.PEKI_API_KEY}` },
body: form,
})).json();

await fetch(`https://peki.ai/api/sessions/${sessionId}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.PEKI_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ text: 'The box arrived damaged', attachments: [file.id] }),
});

Response 201:

{ "id": "671ca1c4a1b2c3d4e5f60730", "name": "damaged-box.jpg", "contentType": "image/jpeg", "size": 284113, "kind": "chat" }

Errors carry a code:

StatuscodeMessage
400Attach a file in the field "file"
400file_typeThis kind of file is not accepted
413file_sizeThe file is larger than 10 MB
400conversation_fullThis conversation already has 30 files
409Only sessions started through the API take messages here
tip

Only some models can see images. If your users will send photos, pick a model marked Reads images for the agent. Other models get a note that a photo was sent. See Files and attachments.

Rate the session​

POST /sessions/{id}/feedback

Records your user's thumbs up or down for the session. The last rating wins. It shows in Analytics as customer satisfaction.

FieldTypeRequiredRules
ratingstringYesup or down. No other fields are allowed.
curl -X POST https://peki.ai/api/sessions/671c9f20a1b2c3d4e5f60718/feedback \
-H "Authorization: Bearer $PEKI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"rating": "up"}'

Response 200 with the session, its feedback set. 409 Only sessions started through the API take feedback here for other channels.

Resolve the session​

POST /sessions/{id}/resolve

Marks the session as resolved, like Resolve in the Inbox. Call it when your user closes the chat or says the problem is solved. If the user writes again in the same session, it opens again.

No body.

curl -X POST https://peki.ai/api/sessions/671c9f20a1b2c3d4e5f60718/resolve \
-H "Authorization: Bearer $PEKI_API_KEY"

Response 200 with the session, status resolved.

Your team and API sessions​

  • API sessions show in the Inbox with the channel API and your user's name or id.
  • When your agent hands off to a person, someone on your team can Take over and reply. Their replies appear in List messages with role team and the author's name. Poll for them while the session is taken over: reply is null and no AI reply comes.
  • After Hand back to AI, the agent answers again.

A complete example​

A small Node.js backend endpoint that your app's chat screen calls:

import express from 'express';

const app = express();
app.use(express.json());
const PEKI = 'https://peki.ai/api';
const AGENT_ID = process.env.PEKI_AGENT_ID;
const headers = { Authorization: `Bearer ${process.env.PEKI_API_KEY}`, 'Content-Type': 'application/json' };

// Your app calls this when the user opens a new chat.
app.post('/chat/sessions', async (req, res) => {
const r = await fetch(`${PEKI}/agents/${AGENT_ID}/sessions`, {
method: 'POST', headers,
body: JSON.stringify({ customer: { id: req.user.id, name: req.user.name } }),
});
res.status(r.status).json(await r.json());
});

// Your app calls this for each message and reads the stream.
app.post('/chat/sessions/:id/messages', async (req, res) => {
const r = await fetch(`${PEKI}/sessions/${req.params.id}/messages`, {
method: 'POST',
headers: { ...headers, Accept: 'text/event-stream' },
body: JSON.stringify({ text: req.body.text }),
});
res.status(r.status).set('content-type', r.headers.get('content-type'));
for await (const chunk of r.body) res.write(chunk);
res.end();
});

app.listen(3000);

Check that req.params.id belongs to the signed-in user before you forward it: the API key can reach every session in your workspace.