REST API

Use your agent from your own backend: answer questions in your app, your mobile app or your own support tools, read conversations and leads, and keep its knowledge up to date. All requests go to 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.

HTTP
Authorization: Bearer idk_live_…
ScopeAllows
chatChatting with agents (POST /agents/:id/chat).
readAgents, conversations, sources and leads.
writeAdding 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.

Secret keys are for servers. The API sends no CORS headers, so browsers can't call it: that keeps keys out of web pages. In the browser, use the chat widget.

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
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" }
  }'
FieldWhat it is
messageRequired. The user's message, up to 8,000 characters.
conversationIdContinues a conversation. Send the same user (or visitorId) as when it started.
userWho'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.
visitorIdAn 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.
metadataExtra context for the agent, like { plan: "pro", current_page: "/billing" }. Up to 1,000 characters; keys that look like secrets (token, password…) are dropped.
streamtrue for server-sent events.

Response

200 OK
{
  "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
  }
}
FieldWhat it is
replyThe agent's answer.
conversationIdSend it back to continue the conversation.
sourcesThe pages the answer came from.
handoffai; waiting when the user asked for a person and your team was alerted; operator once a teammate replies.
collectLeadThe agent would like the user's contact details (Collect Leads is on).
bookCallA booking link the agent offered, or null.
Node.js
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;
Python
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.

text/event-stream
event: token
data: {"token":"Your order "}

event: token
data: {"token":"shipped yesterday."}

event: done
data: {"conversationId":"conversations_…","messageId":"messages_…","reply":"Your order shipped yesterday.", …}
Node.js
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.

200 OK
{
  "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).

curl
# 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"
typeFields
texttitle, body
qaquestion (or questions, a list of phrasings), answer, optional title
websiteurl, 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.

JSON
{
  "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."
  }
}
codeStatusMeaning
invalid_request400A field is missing or wrong; the message says which.
model_not_available400The model isn't one IntelliDesk offers. Pick one from the model list in Settings → AI.
unauthorized401No key, a malformed key, or a revoked one.
out_of_credits402The 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.
forbidden403The key lacks the scope or can't use this agent.
owner_unverified403The workspace owner hasn't verified their email yet.
not_found404No such agent, conversation or endpoint.
conversation_not_found404The conversationId isn't this agent's, or was started by another user or visitorId.
rate_limited429Too many requests. Wait for the Retry-After header (seconds).
internal_error500Something 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.