REST API
https://YOUR_APP/api/v1. For Postman, Insomnia or a generated client, import the OpenAPI spec.Authentication
Create a secret key under API keys in the dashboard, and send it as a bearer token. Keys start with idk_live_. The full key is shown once, when you create it.
Authorization: Bearer idk_live_…| Scope | Allows |
|---|---|
| chat | Chatting with agents (POST /agents/:id/chat). |
| read | Agents, conversations, sources and leads. |
| write | Adding sources and retraining. |
A key can also be limited to some agents. It stops working when it's revoked or when the person who made it leaves the workspace.
Chat with an agent
POST /agents/:id/chat, scope chat. :id is the agent's id or slug. The reply comes from the agent's trained knowledge, instructions and actions, exactly as on your website, and the conversation appears in Activity.
curl https://YOUR_APP/api/v1/agents/AGENT_ID/chat \
-H "Authorization: Bearer $INTELLIDESK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Where is my order?",
"user": { "id": "u_123", "email": "ada@example.com", "name": "Ada" }
}'| Field | What it is |
|---|---|
| message | Required. The user's message, up to 8,000 characters. |
| conversationId | Continues a conversation. Send the same user (or visitorId) as when it started. |
| user | Who's asking: { id, email?, name?, phone?, attributes? }. Ties the chat to their contact in IntelliDesk. The key vouches for it, so no hash is needed. id: up to 256 visible characters. |
| visitorId | An anonymous id of your choosing, when you don't know who the user is. Without user or visitorId, all of a key's chats share one visitor. |
| metadata | Extra context for the agent, like { plan: "pro", current_page: "/billing" }. Up to 1,000 characters; keys that look like secrets (token, password…) are dropped. |
| stream | true for server-sent events. |
Response
{
"data": {
"conversationId": "conversations_…",
"messageId": "messages_…",
"reply": "Your order shipped yesterday. Here's the tracking link: …",
"sources": [{ "title": "Shipping policy", "url": "https://example.com/shipping" }],
"handoff": "ai",
"collectLead": false,
"bookCall": null
}
}| Field | What it is |
|---|---|
| reply | The agent's answer. |
| conversationId | Send it back to continue the conversation. |
| sources | The pages the answer came from. |
| handoff | ai; waiting when the user asked for a person and your team was alerted; operator once a teammate replies. |
| collectLead | The agent would like the user's contact details (Collect Leads is on). |
| bookCall | A booking link the agent offered, or null. |
const res = await fetch(`https://YOUR_APP/api/v1/agents/${agentId}/chat`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INTELLIDESK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
message: question,
conversationId, // from the previous reply, to continue the chat
user: { id: user.id, email: user.email, name: user.name },
}),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
const { reply, conversationId: next } = body.data;import os, requests
res = requests.post(
f"https://YOUR_APP/api/v1/agents/{agent_id}/chat",
headers={"Authorization": f"Bearer {os.environ['INTELLIDESK_API_KEY']}"},
json={"message": question, "conversationId": conversation_id, "user": {"id": user_id}},
timeout=60,
)
body = res.json()
if not res.ok:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
reply = body["data"]["reply"]Streaming
With "stream": true, the reply arrives as server-sent events while it's written: a token event for each piece, then done with the same fields as a normal response, or error with status, code and message.
event: token
data: {"token":"Your order "}
event: token
data: {"token":"shipped yesterday."}
event: done
data: {"conversationId":"conversations_…","messageId":"messages_…","reply":"Your order shipped yesterday.", …}const res = await fetch(`https://YOUR_APP/api/v1/agents/${agentId}/chat`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ message: question, stream: true }),
});
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
let end;
while ((end = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
const event = /^event: (.*)$/m.exec(frame)?.[1];
const data = JSON.parse(/^data: (.*)$/m.exec(frame)?.[1] || "{}");
if (event === "token") process.stdout.write(data.token);
if (event === "done") console.log("\nconversation:", data.conversationId);
if (event === "error") throw new Error(`${data.code}: ${data.message}`);
}
}Conversations
GET /agents/:id/conversations, scope read: newest first, from every channel (website, help page, email, API…). limit is 1 to 100 (default 25). For the next page, pass before= the nextBefore value; it's null on the last page.
{
"data": [
{
"id": "conversations_…",
"agentId": "agents_…",
"channel": "widget",
"visitorId": "v_…",
"createdAt": 1790467905529,
"lastMessageAt": 1790468010311,
"status": "open",
"summary": "Asked where their order is; shared tracking.",
"sentiment": "positive",
"resolution": "resolved",
"rating": 5
}
],
"nextBefore": 1790468010311
}status is open, with_human or ended. sentiment is positive, neutral or negative; resolution is resolved, unresolved, escalated or in_progress; rating is the visitor's 1 to 5 score, when they gave one.
GET /conversations/:id returns one conversation with its messages: role (visitor, agent or teammate), content, createdAt, sources, and verified when the answer was checked against the knowledge.
Agents
GET /agents lists the agents the key can use; GET /agents/:id returns one: id, name, slug, model, trainedStatus (untrained, training, trained or retraining_required), lastTrainedAt and source totals. Scope read.
Knowledge sources
GET /agents/:id/sources (scope read) lists them with their status and size. POST /agents/:id/sources (scope write) adds one and answers 201 with its id. New sources are used after a retrain: POST /agents/:id/retrain answers 202 and training runs in the background (watch trainedStatus).
# Text
curl https://YOUR_APP/api/v1/agents/AGENT_ID/sources -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d '{"type":"text","title":"Returns","body":"Returns are free within 30 days."}'
# Q&A
curl … -d '{"type":"qa","question":"Do you ship abroad?","answer":"Yes, to 30 countries."}'
# Website (crawl, sitemap or a single link)
curl … -d '{"type":"website","url":"https://example.com/help","mode":"crawl","includePaths":["/help"]}'
# Then retrain, so the agent uses it
curl -X POST https://YOUR_APP/api/v1/agents/AGENT_ID/retrain -H "Authorization: Bearer $KEY"| type | Fields |
|---|---|
| text | title, body |
| qa | question (or questions, a list of phrasings), answer, optional title |
| website | url, mode (crawl, sitemap or link), optional includePaths / excludePaths |
Leads
GET /agents/:id/leads, scope read: the newest 200 contact details visitors left, with name, email, phone, customFields, conversationId, createdAt and score (hot, warm or cold). To get each one as it arrives, use the leads.submit webhook.
Errors
Errors use HTTP status codes and always have the same shape. Switch on code; message is for people and may change.
{
"error": {
"status": 404,
"code": "conversation_not_found",
"message": "Conversation not found for this agent. Continue a conversation with the same user, visitorId or key that started it."
}
}| code | Status | Meaning |
|---|---|---|
| invalid_request | 400 | A field is missing or wrong; the message says which. |
| model_not_available | 400 | The model isn't one IntelliDesk offers. Pick one from the model list in Settings → AI. |
| unauthorized | 401 | No key, a malformed key, or a revoked one. |
| out_of_credits | 402 | The workspace has used its message credits. They refill at the start of the next period, or the owner can choose a bigger plan under Billing. |
| forbidden | 403 | The key lacks the scope or can't use this agent. |
| owner_unverified | 403 | The workspace owner hasn't verified their email yet. |
| not_found | 404 | No such agent, conversation or endpoint. |
| conversation_not_found | 404 | The conversationId isn't this agent's, or was started by another user or visitorId. |
| rate_limited | 429 | Too many requests. Wait for the Retry-After header (seconds). |
| internal_error | 500 | Something failed on our side. Safe to retry. |
Rate limits
120 requests a minute per key, for all endpoints together. Over that, you get 429 rate_limited with a Retry-After header. Chats through the API don't count against the per-visitor limits of the website widget, so one key can serve all of your users.
Using an AI assistant?
The same keys work with the MCP server, so Claude, Cursor and other assistants can ask your agent and read its conversations.