Website chat API
The website chat API is what the Peki chat bubble uses on your site. It's public by design: it runs in your visitors' browsers, with no API key. If the ready-made widget doesn't fit your design, you can build your own chat UI on these endpoints. Most sites should use the snippet instead; see Website chat.
Base URL: https://peki.ai/api/widget/{key}
Auth: none. The widget key in the path and an x-visitor-id header.
Never put an API key (psk_…) in a web page. The website chat API needs none. If your page needs more than a visitor's own chat, call the Sessions API from your server.
Before you start
-
Turn on the website chat for the agent: in Channels → Website chat, switch on Show the chat on your website.
-
Copy the widget key from the snippet. In
<script src="https://peki.ai/widget.js" data-key="pw_Xk3n9QeR7tLm2vBc" async></script>the key is
pw_Xk3n9QeR7tLm2vBc. -
Allow your site. Add your site's address, like
https://shop.example.com, under Allowed websites with Add website. If the list is empty, any website can use the chat. -
Publish the agent. Until then, the chat answers
409 This assistant is not available yet.
The visitor id
Every request needs an x-visitor-id header: a random id that you create once in the visitor's browser and keep, for example in localStorage. It's the visitor's only credential: whoever has it can read that visitor's conversation, and nothing else.
- 16 to 64 characters: letters, digits,
-and_. - Make it random and unguessable, like a UUID without braces.
- Keep it per browser. A new id means a new, empty chat.
function visitorId() {
let id = localStorage.getItem('peki-visitor');
if (!id) {
id = crypto.randomUUID().replaceAll('-', '');
localStorage.setItem('peki-visitor', id);
}
return id;
}
A missing or malformed id gets 400 Missing or invalid visitor id.
Allowed sites (CORS)
The browser sends your page's address in the Origin header. If the widget has Allowed websites, only those exact addresses (scheme, host and port, like https://shop.example.com) get through; everything else gets:
{ "statusCode": 403, "message": "This website may not use this chat" }
Add every address your chat runs on, including https://www. variants and staging sites. Up to 20.
Allowed methods are GET, POST and OPTIONS; allowed request headers are content-type and x-visitor-id.
Endpoints
| Method | Path | What it does |
|---|---|---|
GET | /config | The chat's name, color, welcome text and whether it's available |
GET | /messages | The visitor's current conversation |
POST | /messages | Send a message |
POST | /files | Upload a file |
GET | /files/{id} | Download a file of the visitor's own conversation |
POST | /sessions | Start a new conversation |
GET | /feedback | Whether to ask for a rating now |
POST | /feedback | Rate the conversation |
A widget that is turned off, or an unknown key, answers every endpoint with 404 This chat is not available.
Get the configuration
GET /config
curl https://peki.ai/api/widget/pw_Xk3n9QeR7tLm2vBc/config
{
"name": "Bright Smile Dental",
"color": "#1C1A17",
"welcome": { "en": "Hi! How can we help you today?", "tr": "Merhaba! Size nasıl yardımcı olabiliriz?" },
"available": true,
"layout": "bubble"
}
| Field | Description |
|---|---|
name | The agent's name. |
color | The chat's color, as #RRGGBB. |
welcome | The welcome text per language (en, tr). Show the one matching your visitor. |
available | false until the agent is published. |
layout | bubble (a button in the corner) or inline (a chat box in the page). |
/config doesn't need a visitor id.
Read the conversation
GET /messages
x-visitor-id: {visitorId}
Returns the visitor's current conversation, oldest first. After New conversation, or once a conversation has gone quiet long enough to end, the list starts empty.
[
{ "id": "671ca0a1a1b2c3d4e5f60720", "from": "visitor", "text": "Do you have an opening tomorrow?", "at": "2026-10-26T14:05:40.102Z" },
{ "id": "671ca0b3a1b2c3d4e5f60722", "from": "agent", "text": "", "at": "2026-10-26T14:05:40.230Z", "pending": true }
]
| Field | Description |
|---|---|
id | Message id. |
from | visitor, agent or team (a person who took over in the Inbox). |
text | The text. Empty while pending. |
at | When it was written. |
pending | true while the agent is still writing. Show a typing indicator. |
author | For team messages: the team member's first name. |
attachments | Files: { fileId, name, contentType, size }. |
Replies arrive by polling. The Peki widget checks every 1.2 seconds while a reply is pending and every 4 seconds otherwise; do the same.
Send a message
POST /messages
x-visitor-id: {visitorId}
Content-Type: application/json
| Field | Type | Required | Rules |
|---|---|---|---|
text | string | One of the two | Up to 2,000 characters. |
attachments | string[] | One of the two | Up to 5 different file ids from Upload a file. |
const KEY = 'pw_Xk3n9QeR7tLm2vBc';
const API = `https://peki.ai/api/widget/${KEY}`;
async function send(text) {
const res = await fetch(`${API}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-visitor-id': visitorId() },
body: JSON.stringify({ text }),
});
if (!res.ok) throw new Error((await res.json()).message);
}
Response 202 { "ok": true }. Then poll GET /messages for the reply.
The first message starts a conversation; later ones continue it until it's resolved or has been quiet for the agent's new-session time.
Upload a file
POST /files
x-visitor-id: {visitorId}
Content-Type: multipart/form-data
One part named file. Photos (JPEG, PNG, WebP, GIF) and documents (PDF, TXT, Markdown, CSV), up to 10 MB. The type is checked from the content. Then send the returned id in a message's attachments within 24 hours.
const form = new FormData();
form.append('file', input.files[0]);
const res = await fetch(`${API}/files`, { method: 'POST', headers: { 'x-visitor-id': visitorId() }, body: form });
const file = await res.json(); // { id, name, contentType, size, kind: "chat" }
Download a file
GET /files/{id}
x-visitor-id: {visitorId}
Returns a file of the visitor's own conversations: their own uploads and files your team sent them. Anything else, whether it exists or not, is 404 File not found (file_missing). Because it needs the header, load files with fetch and show them from a blob, not with a plain <img src>.
Start a new conversation
POST /sessions
x-visitor-id: {visitorId}
The visitor's next messages go to a fresh conversation. The old one stays in your Inbox. Response { "ok": true }.
Ratings
GET /feedback
x-visitor-id: {visitorId}
{ "ask": true, "rating": null }
ask is true when it makes sense to show thumbs up and down: the agent has answered and the conversation was resolved or has been quiet for 2 minutes. rating is the rating already given, if any.
POST /feedback
x-visitor-id: {visitorId}
Content-Type: application/json
| Field | Type | Required | Rules |
|---|---|---|---|
rating | string | Yes | up or down. |
comment | string | No | Up to 500 characters. |
No other fields are allowed. Response { "ok": true }. The last rating wins. 409 Nothing to rate yet if there's no answered conversation from the last 24 hours.
Limits
The chat protects itself against floods. Over a limit, the request gets 429 with a message you can show the visitor.
| What | Limit | Message |
|---|---|---|
| Messages per visitor | 20 per minute | You are sending messages too fast. Please wait a moment. |
| Messages from one network to one chat | 200 per hour | Too many messages from this network. Please try again later. |
| Messages to one chat in total | 120 per minute | This chat is very busy right now. Please try again in a minute. |
| New conversations from one network | 20 per hour | Too many new chats from this network. Please try again later. |
| Files per visitor | 20 per hour | file_rate |
| Files from one network | 60 per hour per chat, 150 per hour in total | file_rate |
| File downloads | 60 per minute per visitor, 600 per minute per network | Too many files opened. Please wait a moment. |
| Ratings | 10 per minute per visitor, 100 per hour per network, 120 per minute per chat | Too many taps. Please wait a moment. |
Files are also limited to 5 per message and 30 per conversation, and the workspace has a daily limit of live replies.
Errors
| Status | Message or code | Why |
|---|---|---|
400 | Missing or invalid visitor id | No x-visitor-id, or not 16–64 letters, digits, -, _. |
400 | Write a message or attach a file | Empty message. |
400 | file_count, file_type, conversation_full | Too many files, a type that isn't accepted, or 30 files already. |
403 | This website may not use this chat | Your site isn't in Allowed websites. |
404 | This chat is not available | Unknown key, or the chat is turned off. |
409 | This assistant is not available yet | The agent isn't published. |
409 | file_taken | A file is already attached or can't be found. |
413 | file_size | The file is larger than 10 MB. |
429 | see Limits | Too many requests. |