Repository navigation
API Reference Chat
Talk to the assistant as a visitor, from your own server: start a conversation, send messages (JSON or streamed), close it. Part of the API reference: setup, shared types and errors are there.
-
Start a conversation:
POST /conversations -
Send a message:
POST /conversations/:id/messages -
Close a conversation:
POST /conversations/:id/end
POST /conversations · scope chat
Starts a conversation as a visitor would, with the same assistant, knowledge, limits and recording as the widget. With message, the answer has the assistant's reply; send Accept: text/event-stream to stream it: delta events with { text } as it is written, then done with this body (or error with the error), when the AI backend streams; otherwise the answer is plain JSON. contact becomes (or joins, by email) a lead. Counts against the site's daily cap.
| Body field | Required | Description |
|---|---|---|
message |
no | The visitor's first message (up to 4000 characters). Without one, the answer is the greeting, if any. |
contact |
no | What you know about the visitor: name, email, phone and any other fields (up to 20). Becomes, or joins by email, a lead. |
user |
no | A signed-in visitor, as your server knows them: id (your user id) and any fields (email, plan, up to 20). Trusted, since the key is the proof: tools and the prompt read it as {{user.*}}, and its email and name become the lead's. From a browser, use a signed token instead (HelpPuff.identify({ token })). |
context |
no |
pageUrl, pageTitle, referrer, locale, timezone, utm: where the visitor is. The assistant may use it. |
externalId |
no | Your own id for this conversation or visitor (up to 128 characters), to find it again. |
metadata |
no | Up to 20 string values you want back later. Never shown to the assistant. |
site |
no | Only with the admin key and several sites; a key uses its own. |
Request
curl -X POST "$HELPPUFF_URL/api/v1/conversations" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "How much is a blocked drain?",
"contact": {
"name": "Ada Lovelace",
"email": "ada@example.com"
},
"context": {
"pageUrl": "https://app.example.com/help",
"locale": "en-AU"
},
"externalId": "user-123",
"metadata": {
"plan": "pro"
}
}'type StartConversationRequest = {
/**
* The visitor's first message (up to 4000 characters). Without one, the
* answer is the greeting, if any.
*/
message?: string;
/**
* What you know about the visitor: `name`, `email`, `phone` and any other
* fields (up to 20). Becomes, or joins by email, a lead.
*/
contact?: Record<string, string>;
/**
* A signed-in visitor, as your server knows them: `id` (your user id) and
* any fields (`email`, `plan`, up to 20). Trusted, since the key is the
* proof: tools and the prompt read it as `{{user.*}}`, and its email and
* name become the lead's. From a browser, use a signed token instead
* (`HelpPuff.identify({ token })`).
*/
user?: string;
/**
* `pageUrl`, `pageTitle`, `referrer`, `locale`, `timezone`, `utm`: where the
* visitor is. The assistant may use it.
*/
context?: {
pageUrl?: string;
locale?: string;
};
/**
* Your own id for this conversation or visitor (up to 128 characters), to
* find it again.
*/
externalId?: string;
/**
* Up to 20 string values you want back later. Never shown to the assistant.
*/
metadata?: Record<string, string>;
/** Only with the admin key and several sites; a key uses its own. */
site?: string;
};
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'How much is a blocked drain?',
contact: {
name: 'Ada Lovelace',
email: 'ada@example.com'
},
context: {
pageUrl: 'https://app.example.com/help',
locale: 'en-AU'
},
externalId: 'user-123',
metadata: {
plan: 'pro'
}
} satisfies StartConversationRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as StartConversationResponse;Response 201
{
"id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
"messages": [
{
"id": "m_mfx2k1",
"ts": 1760000000000,
"role": "agent",
"type": "text",
"text": "A blocked drain is usually $180–$250, including the first hour. Would you like a callback to book it in?"
}
],
"externalId": "user-123",
"metadata": {
"plan": "pro"
}
}type StartConversationResponse = {
id: string;
messages: Message[];
externalId: string | null;
metadata: Record<string, string> | null;
};Errors
| Status | Code | When |
|---|---|---|
| 429 | quota_exceeded |
The site used its daily cap (security.limits.messagesPerSitePerDay). |
| 502 | connector_error |
The AI backend failed; try again. |
POST /conversations/:id/messages · scope chat
The visitor's next message: { "text" }, or { "action": { id, value, label? } } to answer options, a card button or a form (a form's answers are a JSON object string in value; forms are honoured only when this conversation was shown them). Only conversations started over the API can be continued here. Accept: text/event-stream streams the reply.
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The record's id. |
| Body field | Required | Description |
|---|---|---|
text |
no | What the visitor typed (up to maxMessageLength). Or, instead: |
action |
no |
{ id, value, label? }: an answer to options, a card button or a form. id is the message (or action) id; a form's answers are a JSON object string in value. |
Request
curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/messages" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Can someone come tomorrow morning?"
}'type SendMessageRequest = {
/** What the visitor typed (up to `maxMessageLength`). Or, instead: */
text?: string;
/**
* `{ id, value, label? }`: an answer to options, a card button or a form.
* `id` is the message (or action) id; a form's answers are a JSON object
* string in `value`.
*/
action?: { id: string; value: string; label?: string };
};
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations/${id}/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: 'Can someone come tomorrow morning?'
} satisfies SendMessageRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as SendMessageResponse;Response 200
{
"id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
"messages": [
{
"id": "m_mfx2k1",
"ts": 1760000000000,
"role": "agent",
"type": "text",
"text": "Tomorrow morning works. What is the best number to reach you on?"
}
]
}type SendMessageResponse = {
id: string;
messages: Message[];
};Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request |
Empty or too long (maxMessageLength), or a form this conversation was not shown. |
| 404 | not_found |
No conversation started over the API with this id. |
| 429 | quota_exceeded |
The conversation reached messagesPerSession, or the site its daily cap. |
POST /conversations/:id/end · scope chat
Ends it for the AI backend and sends conversation.ended (once). It stays readable.
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The record's id. |
Request
curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/end" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations/${id}/end`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
},
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as CloseConversationResponse;Response 200
{
"id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
"ended": true
}type CloseConversationResponse = {
id: string;
ended: boolean;
};Start
Tutorials
- All tutorials
- Order tracking
- Send chats to your CRM
- Nearest store
- Verified account changes
- Logged-in customer support
- Product and stock
- Hot leads to Slack
- Knowledge base or tool?
- Testing and monitoring
- Why the AI never books
Configure
- Configuration
- Configuration reference
- CLI reference
- Knowledge base
- Prompt and instructions
- Tools: your APIs in the chat
- Agent files: import and export
- Signed-in visitors
- The widget
- The playground
- Leads and callbacks
- Jobs
- Photos and files
- Bookings and the calendar
- Integrations
- Live chat
- Telegram
- Webhooks
- Email notifications
API
API reference
- Overview
- Account
- Chat
- Conversations
- Live chat
- Labels and notes
- Leads
- Callbacks
- Jobs
- Appointments
- Integrations
- Knowledge
- Prompt
- Tools
- Settings
- Webhooks
- Analytics
- Team
- API keys
- Audit
Providers
- Overview
- Models and providers
- Custom model
- Custom knowledge base
- Workers AI (default)
- AI models: speed, quality, cost
- Cloudflare AI Search
- OpenAI
- Gemini
- Anthropic Claude
- Retell
- Your own API
Operate
- Deployment
- Turn on Turnstile
- Upgrading
- Costs and limits
- Cloudflare Free plan limits
- Security
- Troubleshooting
Build