Contents

API reference

Every endpoint of the REST API, with what it takes, what it returns and which scope it needs.

All paths are relative to https://chatterlab.ai/api/v1. Authentication, errors, limits and paging are described once, in the API overview.

Method and pathScopeWhat it does
GET /workspacereadThe key, and the workspace's plan and usage
GET /modelsreadThe models an agent can use
GET /setup-guidereadHow to set an agent up for what it is for
GET /agentsreadList agents
POST /agentsmanageCreate an agent
GET /agents/{id}readOne agent
PATCH /agents/{id}manageChange an agent
DELETE /agents/{id}manageDelete an agent and everything it holds
PUT /agents/{id}/branding/{image}manageSet the logo or the launcher image from an address
DELETE /agents/{id}/branding/{image}manageRemove the logo or the launcher image
GET /agents/{id}/embedreadThe install snippets
GET /agents/{id}/sourcesreadList sources
POST /agents/{id}/sourcesmanageAdd a website, a file, a text or a Q&A pair
GET /agents/{id}/sources/{sourceId}readOne source, with its content
DELETE /agents/{id}/sources/{sourceId}manageDelete a source
GET /agents/{id}/crawlreadProgress of the latest crawl
DELETE /agents/{id}/crawlmanageStop the latest crawl
POST /agents/{id}/retrainmanageRead every source again
POST /agents/{id}/chatchatAsk a question
GET /agents/{id}/conversationsreadList conversations
GET /agents/{id}/conversations/{conversationId}readOne conversation with its messages
DELETE /agents/{id}/conversations/{conversationId}manageErase one conversation
GET /agents/{id}/conversations/exportreadA link to a PDF of the conversations in a period
GET /agents/{id}/visitor-files/{fileId}readA file a visitor sent, with the text read from it
DELETE /agents/{id}/visitor-files/{fileId}manageErase a file a visitor sent
GET /agents/{id}/leadsreadList the contact details visitors left
GET /agents/{id}/leads/{leadId}readOne lead
DELETE /agents/{id}/leads/{leadId}manageErase one lead
GET /agents/{id}/ticketsreadList tickets
GET /agents/{id}/tickets/{ticketId}readOne ticket with what came after the question
PATCH /agents/{id}/tickets/{ticketId}manageChange a ticket's status
POST /agents/{id}/tickets/{ticketId}/notesmanageAdd a note to a ticket
GET /agents/{id}/clientreadThe client an agent is run for
PUT /agents/{id}/clientmanageRecord a client, or change what is recorded
DELETE /agents/{id}/clientmanageTake the client off an agent
POST /agents/{id}/client/report-linkmanageGive the client a new report link
POST /agents/{id}/messages/{messageId}/correctionmanageCorrect one answer
GET /agents/{id}/analyticsreadFigures for a period

Workspace and models

GET /workspace returns key (id, name, prefix, scope, agentId) and workspace: name, plan, default language, the number of agents and the limit, credits used this month, credits included, purchased credits, overage (always false: overage was removed, and extra credits come from packs), and storage per agent. A key tied to one agent gets key only.

GET /models returns the models with their id, provider, creditsPerAnswer and contextSize. available says whether the workspace's plan can use the model. A key tied to one agent only gets available if its scope is manage.

Agents

GET /setup-guide is where to start before creating an agent. It returns presets, one for each use case: its purpose, instructions and a welcomeMessage to adapt, with every [bracketed] part left for you to fill in, the temperature and retrieval settings that suit it, features (whether booking and visitorFiles are recommended, optional or not_needed, and why), the sources to gather, ten testQuestions of which the agent should decline the last two, and the checks its answers should pass. ?useCase=support returns that one alone; a high-risk use case is refused with high_risk_use_case, and any other value with invalid_request. With them comes plan: its id, agents (count, limit and left, null where there is no limit), storagePerAgentBytes, visitorFiles, whether visitors may send files on this plan, and brandingRemoval. A key tied to one agent gets presets only. The presets are written in English: write the agent's own instructions in its visitors' language.

POST /agents

Field
namerequired1 to 80 characters
useCaserequiredgeneral, support, sales or docs. The high-risk values hr_recruitment, credit_scoring and healthcare are refused with high_risk_use_case. Why
descriptionup to 300 characters
instructionsup to 20,000 characters
welcomeMessageup to 300 characters
languageen, nl, fr, de or es
websiteAn address to crawl straight away, as a first source. The reply then also has crawl (its id and status), or crawlError if the crawl could not start. The agent is created either way.
curl -X POST https://chatterlab.ai/api/v1/agents \
  -H "Authorization: Bearer cl_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name":"Support","useCase":"support","language":"en","website":"https://example.com"}'

The agent object has id, name, description, useCase (what it was declared for when it was made, null for some agents made before 23 September 2026), instructions, welcomeMessage, language, languages (the languages the widget may speak, language first: it follows a visitor's browser into the others), model, temperature (0 to 1), retrieval (topK 1 to 10, minSimilarity 0 to 1), allowedOrigins, calendlyUrl, attachmentsEnabled, branding, proactive (the greeting and second nudge: greeting, nudge, pages, paths and mobile, with every default filled in), leadCapture (the contact form: enabled, fields, timing, skippable, afterQuestions, afterWeakAnswer, button, intro, thanks, privacyUrl and marketing, with every default filled in), tickets (the tickets in the chat: enabled, answerTime, empty for the standard sentence, and privacyUrl), status (active or paused), lastTrainedAt, createdAt and updatedAt.

PATCH /agents/{id} takes any of name, description, instructions, welcomeMessage, language, languages, model, temperature, retrieval, allowedOrigins, calendlyUrl, attachmentsEnabled, branding, proactive, leadCapture and tickets. Only what you send changes, inside branding, proactive, leadCapture and tickets too. branding.translations gives the welcome message, messagePlaceholder and launcherLabel a version per widget language, such as { "fr": { "welcomeMessage": "Bonjour !" } }, and an empty text removes that version. languages replaces the list, and language is always kept in it. allowedOrigins replaces the whole list, and an empty list opens the agent to every site. An entry such as https://*.example.com covers every subdomain, and one over a public suffix, such as *.com, is refused with invalid_request. A model the plan does not include is refused with model_not_allowed. attachmentsEnabled lets visitors send files in the widget. A leadCapture that is switched on needs a privacyUrl starting with https://, and an e-mail address or a phone number among its fields; without them it is refused with invalid_request. tickets that are switched on need a privacyUrl starting with https://; without it they are refused with invalid_request. Who is e-mailed each lead or ticket is changed in the dashboard only.

DELETE /agents/{id} removes the agent with its sources, conversations, files and tickets. It cannot be undone.

PUT /agents/{id}/branding/logo sets the logo in the chat panel's header from an image at an address, { "url": "https://..." }, and PUT /agents/{id}/branding/icon does the same for the image on the launcher. The image has to be a PNG, JPEG, GIF or WebP of at most 512 KB. Nothing is cropped or shrunk on the way, so a larger image is refused with file_too_large: upload it in the dashboard instead, where it is cropped. An SVG is taken only as an upload in the dashboard. DELETE on the same address removes the image. Each answers with the agent.

GET /agents/{id}/embed returns script and iframe, the two snippets from the Deploy page, and allowedOrigins.

Sources

POST /agents/{id}/sources takes one of four bodies, told apart by type:

{ "type": "website", "url": "https://example.com", "mode": "links", "excludePaths": ["/blog"] }
{ "type": "text", "title": "Delivery times", "content": "We deliver within..." }
{ "type": "qa", "question": "Can I still cancel?", "answer": "Yes, until the order ships." }
{ "type": "file", "url": "https://example.com/downloads/manual.pdf", "name": "Manual" }

mode is links (follow links on the same site, the default), sitemap or single. The limits are those of the dashboard. See Websites, Files and Text and Q&A.

A file is fetched from its address, since there is no browser here to upload it. It has to be a PDF, a Word document (.docx), Markdown or plain text, which is told from the file itself rather than from its name. It has to arrive within 20 seconds, and may be as large as the plan's storage per agent, at most 50 MB. name is optional and defaults to the last part of the address. A file that is only on someone's computer is uploaded under Sources in the dashboard. A refused file says why: unsupported_file_type, storage_limit_reached, file_too_large, unsafe_url or fetch_failed.

A text, a Q&A pair or a file comes back as a source with status pending. Poll GET .../sources/{sourceId} until it is ready or failed, which usually takes seconds, and a minute or more for a long file. A website comes back as a crawl: follow it with GET /agents/{id}/crawl.

The source object has id, type (website, file, text or qa), name, url, status, error, turnedAway (login_page or too_little_content for a page a later crawl turned away, otherwise null), sizeBytes, chunkCount, createdAt and processedAt. A single source also has content. The list leaves it out, to keep pages small. On a failed source, error is one of file_unreadable, no_content, too_large, never_finished, temporary and file_too_complex, the last for a file that took more memory or time to read than a source may use.

The crawl object has id, status (queued, running, completed, failed or cancelled), processed, discovered, pending, failed and stopReason, which is set when it stopped before the site ran out, for example at the storage allowance.

DELETE /agents/{id}/crawl stops the latest crawl where it is, and answers { "id": ..., "cancelled": true, "processed": ... }. The pages already read stay: they are sources, and deleting one is its own request. It is a 404 when the agent has never been crawled, or when its latest crawl had already ended.

POST /agents/{id}/retrain has every source read again from what was stored when it was added, as Retrain agent does. It answers 202 with { "id": ..., "queued": true, "sourceCount": ... }, and the sources go back to pending and are processed over the next minutes. It does not fetch the website again, and it costs no credits. While a source is still being uploaded, waiting or read, or a crawl is under way, it is a 409 with agent_busy, as the button is greyed out then. When it helps

Chat

POST /agents/{id}/chat

Field
messagerequiredThe question, up to 4,000 characters
conversationIdContinue an earlier conversation. Leave it out to start one.
userIdYour own stable id for the person asking: letters, digits and dashes, up to 64. It groups conversations in Activity. Do not put an e-mail address in it.
languageen, nl, fr, de or es: the language to answer in, whatever language the question is in. Without it, or with another value, the agent answers in the language of the question, and in its own when the question shows none.
streamtrue for a stream instead of one JSON answer
testtrue files the question with your test conversations (channel playground) instead of api, which keeps it out of the Overview and client reports
page{ "url": "..." }, the page of your site the person is looking at. When the agent has read it as a source, its passages come first. Its query string and fragment are never read, and an address that is not http or https is ignored.
curl -X POST https://chatterlab.ai/api/v1/agents/ag_123/chat \
  -H "Authorization: Bearer cl_your_key" \
  -H "Content-Type: application/json" \
  -d '{"message":"Do you deliver to Belgium?"}'
{
  "data": {
    "conversationId": "conv_...",
    "messageId": "msg_...",
    "answer": "Yes. Delivery to Belgium takes two working days.",
    "confidence": 84,
    "grounding": "supported",
    "sources": [ { "id": "src_...", "name": "Delivery", "similarity": 0.61 } ],
    "model": "gpt-oss-120b",
    "credits": 1
  }
}

confidence runs from 0 to 100. grounding is supported, partial, unsupported or null when no check was made. What they mean. Use them: a backend that gets confidence 20 can choose to show "let me connect you to a colleague" instead of the answer.

An answer costs credits exactly as it does in the widget, test or not. The agent remembers the last 10 messages of a conversation.

Streaming. With stream: true the response is a server-sent event stream in the format of the Vercel AI SDK's UI message stream, the same one our widget reads. Confidence and sources arrive at its start. Grounding cannot, since it judges a text that is not written yet: read it afterwards from the conversation.

Conversations

GET /agents/{id}/conversations takes ?channel= (widget, iframe, api or playground), ?from= and ?to= (dates as YYYY-MM-DD, inclusive, filtering on the last message), ?limit= and ?cursor=. Each item has id, channel, userId, startedAt, lastMessageAt, messageCount, firstMessage, lastConfidence and openedBy: launcher, greeting or nudge for a conversation in a chat the button opened, by what the visitor clicked, otherwise null.

DELETE /agents/{id}/conversations/{conversationId} erases the conversation and every message in it, with the lead the visitor left in it and its ticket. Use it to answer a visitor who asks to be forgotten. What the agent did stays in the audit trail and the month's usage does not change; analytics and client reports are worked out from the conversations, so both change.

GET /agents/{id}/conversations/{conversationId} adds messages. Each has id, role, content and createdAt. A question also has attachments, the files the visitor sent with it, usually none. Each has id, fileName, kind, mediaType, sizeBytes, pages, truncated, which is true when the agent read only the first part of it, and ocrPages, the pages of a scan read by text recognition, or null. What a file said is not included. A question also has page, the page it was asked on as { "url", "title" } or null; title is the page's name when the agent knows it as a source. An answer also has confidence, grounding, sources, feedback (1, -1 or null) and model.

POST /agents/{id}/messages/{messageId}/correction takes question and answer, what one of the agent's answers should have said, as Improve answer does in Activity. messageId is the answer's: from a chat reply, or an answer's id in a conversation. It answers 201 with a Q&A source, pending until the agent has learned it. The conversation is not changed. An answer is corrected once: the second time is a 409 with already_corrected, and the pair is changed under Sources. An answer to a question that came with a file cannot be corrected, since a pair cannot carry the file: that is a 422 with asked_with_file. Fixing weak answers

GET /agents/{id}/visitor-files/{fileId} returns one file a visitor sent, by the id a question lists it under in attachments: the same fields, its conversationId, stored, and text, what the agent was given to read from it. text is null for a picture, which the agent looked at rather than read, and after the file's 30 days, when stored is false as well. The file itself is not handed out: it is downloaded in Activity. Reading the text is recorded in the audit trail, as a download is. DELETE on the same address erases the file and its text and leaves the conversation, for whoever may erase a conversation. Files from visitors

GET /agents/{id}/conversations/export returns a link to a PDF of the conversations in a period, the export on the Activity page: url, which works for 15 minutes and downloads the PDF without a key, and expiresAt. It takes ?from= and ?to= as YYYY-MM-DD, UTC days, the last 30 days without them, and ?channel=. At most 500 conversations go in, the most recent. The export is recorded in the audit trail when the link is opened, not when it is made. The link is the credential, so pass it on as you would a password; it stops working at once when the key that asked for it is revoked.

Leads

GET /agents/{id}/leads lists the contact details visitors left in the chat's form, newest first, with ?limit= and ?cursor=. Leads left before a move to a plan without the form can still be read.

The lead object has id, conversationId, name, email, phone and company (null when not asked or left empty), companyDomain (the domain of a business address as a hint of the company, null for an address at a mailbox provider), marketing (whether the visitor ticked the box for news and offers), moment (before, questions, weak_answer or button: what brought the form), page ({ "url", "title" } or null), language, shown (the words the visitor read when they sent it: intro, consent, privacyUrl, and marketing, null when there was no box), createdAt and expiresAt, when it is deleted unless it is deleted sooner.

GET /agents/{id}/leads/{leadId} returns one lead. DELETE on the same address erases it, for whoever may erase a conversation, and leaves the conversation.

Tickets

GET /agents/{id}/tickets lists the tickets visitors opened in the chat, the most recent activity first, with ?status= (open, waiting or solved), ?limit= and ?cursor=.

The ticket object has id, number (counted per agent: what the visitor and the team call it), status, email, name (null when not given), question, language (the widget's when it was opened, which every mail to the visitor is in), signedIn (whether the address came from a signed-in visitor), page ({ "url" } or null), conversationId (null for one opened before the first message), createdAt, lastActivityAt, solvedAt, and expiresAt, when a solved ticket is deleted unless it is deleted sooner, null while it is not solved.

GET /agents/{id}/tickets/{ticketId} returns one ticket with entries, what came after the question, oldest first. Each has id, kind (visitor for a reply from the visitor by e-mail, team for a reply to the visitor, note for a note only the team sees, held for a mail held back until someone adds or discards it in the dashboard), body, from (the address a visitor's reply or a held mail came from, otherwise null), mailError (why a reply to the visitor could not be mailed, otherwise null) and createdAt.

PATCH /agents/{id}/tickets/{ticketId} takes status: open, waiting or solved. Nothing is mailed to the visitor. It answers with the ticket.

POST /agents/{id}/tickets/{ticketId}/notes takes body, up to 10,000 characters, and answers 201 with the note, an entry of kind note.

Replying to a visitor, deleting a ticket on its own and who is e-mailed each ticket are done in the dashboard only. Deleting a conversation deletes its ticket, and deleting an agent all of its tickets.

Clients

These are for an agency's own keys, on Agency and up; below it they are refused with clients_not_in_plan. A key tied to one agent is refused all four, since that is the kind handed to a client, and the fees are on the same record. Clients

GET /agents/{id}/client returns client (name, email, language, since, and serviceFeeEur and setupFeeEur, left out for whoever may not see fees) and report (link, the client's own link to their archive, enabled, day, transcripts, holdForReview, intro and signature), or client: null for an agent the workspace runs for itself.

PUT /agents/{id}/client takes any of name, email, language, serviceFeeEur, setupFeeEur, intro, signature, reportDay (1 to 28), transcripts, reportsEnabled and holdForReview. What you leave out stays as it is, and a first client needs name and email. While reportsEnabled is on, the client is e-mailed a report every month on reportDay. For a first client it is off unless you send true, so that no address starts receiving mail on one call nobody confirmed; the Clients screen, where you see the switch before saving, has it on. The first time, replies to reports go to the workspace owner's address. Sending a report now is not part of the API. It answers as GET does.

DELETE /agents/{id}/client takes the client off: the link to their archive stops working and no more reports are sent. The agent and its conversations stay.

POST /agents/{id}/client/report-link makes a new link to the client's archive and stops the old one, in reports already sent as well. It answers with the client and the new report.link.

Analytics

GET /agents/{id}/analytics takes ?from= and ?to= as YYYY-MM-DD. Without them it covers the last 30 days. A period spans at most 366 days. A date here is a UTC day, which is what the dashboard's own screens are not: those follow the reader's clock, so the same period can differ by a few hours from what someone sees on the Overview. It returns what the Overview page shows: conversations, questions, helpful, notHelpful, averageConfidence, weakShare, a series per day, the three bands, topQuestions and weakest, and widget: the chat button's opened and conversations, each also split by what opened the chat in openedBy and conversationsBy (launcher, greeting, nudge), leads, leadFormsDeclined, and startPages, up to 10 of { "url", "title", "conversations" }. Test conversations are left out of all of them. A key that is not tied to one agent also gets testConversations, the number left out.