Authentication
Create a key under Dashboard → Account → API keys. Send it as a bearer token on every request. Keys carry your account’s full access, so keep them server-side.
curl /api/v1/bots \ -H "Authorization: Bearer cg_live_your_key_here"
Base URL: /api/v1 · All requests and responses are JSON.
Errors
Failed requests return a non-2xx status and a body like {"error": "Chatbot not found."}.
| Status | Meaning |
|---|---|
| 400 | Something in the request is missing or invalid. |
| 401 | Missing or invalid API key. |
| 402 | Your plan limit has been reached (e.g. sources). |
| 404 | The chatbot or conversation doesn’t exist in your account. |
Chat
POST/bots/{botId}/chat
Send a visitor’s message and receive the assistant’s answer. Actions, human handoff and plan limits work exactly as they do in the website widget.
| Field | Type | Description |
|---|---|---|
message | string | Required. The user’s message (max 2,000 characters). |
conversationId | string | Continue an existing conversation. Omit to start a new one. |
userId | string | Your own identifier for the end user. Required to continue a conversation. |
stream | boolean | Stream the reply as server-sent events (see below). |
curl -X POST /api/v1/bots/b_123/chat \
-H "Authorization: Bearer $CUSTGPT_KEY" -H "Content-Type: application/json" \
-d '{"message": "Do you ship to Canada?", "userId": "crm-4821"}'
{
"conversationId": "cv_Q8r7s6",
"userId": "crm-4821",
"messageId": "m_T5u4v3",
"answer": "Yes! Orders to Canada arrive in 4–7 business days [1].",
"sources": [{ "n": 1, "title": "Shipping information", "url": "https://…" }],
"humanHandling": false
}
humanHandling: true means a team member has taken over this conversation. The AI stays silent and your teammate replies from the dashboard.
Streaming responses
With "stream": true the response is text/event-stream. Each data: line is JSON with a type:
| type | Payload |
|---|---|
start | conversationId, userId |
delta | text: the next piece of the answer |
action | name, label, status (running · done · error) when the assistant calls one of your Actions |
done | messageId, the full answer, sources, humanHandling |
Chatbots
GET/bots
List your chatbots with their settings and stats.
GET/bots/{botId}
Get a single chatbot.
Knowledge sources
GET/bots/{botId}/sources
List the pages, files and text a chatbot has learned from.
POST/bots/{botId}/sources/text
Add a block of text. Body: {"title": "Refund policy", "content": "…"}. It’s searchable immediately.
POST/bots/{botId}/sources/qa
Add a custom answer that takes priority for matching questions. Body: {"question": "…", "answer": "…"}.
POST/bots/{botId}/sources/crawl
Import web content in the background. Body: {"url": "https://example.com", "mode": "crawl" | "sitemap" | "single"}. Returns 202 with a crawlId.
Conversations
GET/bots/{botId}/conversations?limit=50
Most recent conversations across all channels (website, Telegram, API).
GET/bots/{botId}/conversations/{conversationId}
A full transcript including AI answers, team replies (role: "agent") and sources.
Leads
GET/leads
Every captured lead across your chatbots, newest first: email, name, phone, status (new · contacted · qualified · won · lost), notes, bot_name, captured_at.
Webhooks
Prefer push over polling? In a chatbot’s Integrations tab, add a webhook URL. We POST JSON for these events:
| Event | When |
|---|---|
lead.created | A visitor shares their contact details. |
handoff.requested | A visitor asks to talk to a person. |
conversation.started | The first question and answer of a new conversation (opt-in). |
test | You pressed “Send test”. |
{
"event": "handoff.requested",
"bot": { "id": "b_123", "name": "Acme Help" },
"data": { "conversationId": "cv_Q8r7s6", "reason": "Wants to change a delivery address", "channel": "web" },
"sentAt": "2026-09-26T09:30:00.000Z"
}