Skip to content

API Reference Live Chat

github-actions[bot] edited this page Oct 8, 2026 · 1 revision

Live chat API

A person on the team answers instead of the assistant: reply, take, give, close and hand back a live chat, and see who is available. Part of the API reference: setup, shared types and errors are there.

Live chat now

GET /live/status · scope conversations:read

Whether live chat is on, who on the team can take chats (an open dashboard set to available), whether Telegram is linked, and how many live chats there are: unassigned, waiting for a reply, and waiting for you.

Parameter In Required Description
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/live/status" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/live/status`, {
  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 LiveChatNowResponse;

Response 200

{
  "enabled": true,
  "hub": true,
  "available": 1,
  "agents": [
    {
      "email": "sam@acme.example",
      "name": "Sam",
      "available": true
    }
  ],
  "telegram": {
    "connected": true,
    "linked": true
  },
  "live": 2,
  "unassigned": 1,
  "waiting": 1,
  "mine": 0
}
type LiveChatNowResponse = {
  enabled: boolean;
  hub: boolean;
  available: number;
  agents: Array<{
    email: string;
    name: string;
    available: boolean;
  }>;
  telegram: {
    connected: boolean;
    linked: boolean;
  };
  live: number;
  unassigned: number;
  waiting: number;
  mine: number;
};

Reply in a live chat

POST /conversations/:id/reply · scope conversations:write

A person on the team answers the visitor (shown with their first name, live.showAgentName). Any conversation: replying to one the assistant has, or a closed one, takes it over first (it becomes live). Needs live chat on. Sends message.sent with author.

Parameter In Required Description
id path yes The record's id.
Body field Required Description
text yes Up to 4000 characters.

Request

curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/reply" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "text": "Hi Ada, Sam here. Tomorrow at 9 works — shall I book it?"
}'
type ReplyInLiveChatRequest = {
  /** Up to 4000 characters. */
  text: string;
};

const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations/${id}/reply`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    text: 'Hi Ada, Sam here. Tomorrow at 9 works — shall I book it?'
  } satisfies ReplyInLiveChatRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as ReplyInLiveChatResponse;

Response 201

{
  "id": "h_mfx2k1a9",
  "ts": 1760000000000,
  "role": "agent",
  "type": "text",
  "text": "Hi Ada, Sam here. Tomorrow at 9 works — shall I book it?",
  "meta": {
    "human": true,
    "agentName": "Sam"
  }
}
type ReplyInLiveChatResponse = {
  id: string;
  ts: number;
  role: string;
  type: string;
  text: string;
  meta: {
    human: boolean;
    agentName: string;
  };
};

Errors

Status Code When
400 bad_request Live chat is off, or the message is empty.

Take a conversation over

POST /conversations/:id/takeover · scope conversations:write

A signed-in person takes the chat, whatever it is doing: from the assistant, from a colleague, or closed (this reopens it). It becomes live and theirs; the visitor sees who joined, at once if their chat is open. For a key, reply instead (POST /conversations/:id/reply takes it over too). Sends conversation.assigned.

Parameter In Required Description
id path yes The record's id.

Request

curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/takeover" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations/${id}/takeover`, {
  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 TakeConversationOverResponse;

Response 200

{
  "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "status": "live",
  "assignedTo": "sam@acme.example",
  "assignedName": "Sam"
}
type TakeConversationOverResponse = {
  conversationId: string;
  status: string;
  assignedTo: string;
  assignedName: string | null;
};

Errors

Status Code When
400 bad_request Live chat is off, or called with an API key (use reply).

Take or give a conversation

POST /conversations/:id/assign · scope conversations:write

to: "me" (a signed-in person takes it), a team member's email (give it; admins only), or null (unassign). The visitor of a live chat sees who joined. Sends conversation.assigned.

Parameter In Required Description
id path yes The record's id.
Body field Required Description
to yes "me", an email on the team, or null.

Request

curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/assign" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "sam@acme.example"
}'
type TakeGiveConversationRequest = {
  /** `"me"`, an email on the team, or null. */
  to: string;
};

const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations/${id}/assign`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: 'sam@acme.example'
  } satisfies TakeGiveConversationRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as TakeGiveConversationResponse;

Response 200

{
  "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "assignedTo": "sam@acme.example",
  "assignedName": "Sam"
}
type TakeGiveConversationResponse = {
  conversationId: string;
  assignedTo: string | null;
  assignedName: string | null;
};

Close a conversation

POST /conversations/:id/close · scope conversations:write

Closes it (conversations also close after live.closeAfterMinutes without a message). The visitor of a live chat is told; if they write again, the assistant answers. Sends conversation.closed.

Parameter In Required Description
id path yes The record's id.

Request

curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/close" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations/${id}/close`, {
  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

{
  "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "status": "closed",
  "closedAt": 1760000000000
}
type CloseConversationResponse = {
  conversationId: string;
  status: string;
  closedAt: number;
};

Hand back to the assistant

POST /conversations/:id/handback · scope conversations:write

Ends the live part, or reopens a closed conversation for the assistant: it answers the visitor's next message. The team can take it over again at any time. Sends handover.ended.

Parameter In Required Description
id path yes The record's id.

Request

curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/handback" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations/${id}/handback`, {
  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 HandBackToAssistantResponse;

Response 200

{
  "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "status": "bot"
}
type HandBackToAssistantResponse = {
  conversationId: string;
  status: string;
};

Errors

Status Code When
409 conflict The assistant already has it.

Telegram

GET /live/telegram · scope settings:read

Whether a Telegram bot is connected and linked to a chat; until linked, the linkCode to send there as /link <code>.

Parameter In Required Description
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/live/telegram" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/live/telegram`, {
  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 TelegramResponse;

Response 200

{
  "connected": true,
  "linked": true,
  "bot": {
    "name": "Acme Desk",
    "username": "acme_desk_bot"
  },
  "chat": {
    "title": "Acme team",
    "topics": true
  },
  "linkCode": null,
  "shareContact": true,
  "status": "ok",
  "lastError": null,
  "updatedAt": 1760000000000
}
type TelegramResponse = {
  connected: boolean;
  linked: boolean;
  bot: {
    name: string;
    username: string;
  } | null;
  chat: {
    title: string;
    topics: boolean;
  } | null;
  linkCode: string | null;
  shareContact: boolean;
  status: string | null;
  lastError: string | null;
  updatedAt: number | null;
};

Connect Telegram

POST /live/telegram · scope settings:write

Connects the bot made with @BotFather by its token (checked with getMe, stored encrypted), and points its webhook here. Then add the bot to your team's group (Topics on, the bot an admin with "Manage topics"), or open a private chat with it, and send /link <linkCode> there.

Body field Required Description
token yes The bot token from @BotFather.

Request

curl -X POST "$HELPPUFF_URL/api/v1/live/telegram" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "token": "7123456789:AAH3k…"
}'
type ConnectTelegramRequest = {
  /** The bot token from @BotFather. */
  token: string;
};

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/live/telegram`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    token: '7123456789:AAH3k…'
  } satisfies ConnectTelegramRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as ConnectTelegramResponse;

Response 201

{
  "connected": true,
  "linked": false,
  "bot": {
    "name": "Acme Desk",
    "username": "acme_desk_bot"
  },
  "chat": null,
  "linkCode": "9f3a1c2e",
  "shareContact": true,
  "status": "linking",
  "lastError": null,
  "updatedAt": 1760000000000
}
type ConnectTelegramResponse = {
  connected: boolean;
  linked: boolean;
  bot: {
    name: string;
    username: string;
  } | null;
  chat: string | null;
  linkCode: string | null;
  shareContact: boolean;
  status: string | null;
  lastError: string | null;
  updatedAt: number | null;
};

Errors

Status Code When
400 bad_request Telegram refused the token, or the webhook.

Change Telegram options

PATCH /live/telegram · scope settings:write

Body field Required Description
shareContact no Include visitors' email and phone in what goes to Telegram.

Request

curl -X PATCH "$HELPPUFF_URL/api/v1/live/telegram" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "shareContact": false
}'
type ChangeTelegramOptionsRequest = {
  /** Include visitors' email and phone in what goes to Telegram. */
  shareContact?: boolean;
};

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/live/telegram`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    shareContact: false
  } satisfies ChangeTelegramOptionsRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as ChangeTelegramOptionsResponse;

Response 200

{
  "connected": true,
  "linked": true,
  "bot": {
    "name": "Acme Desk",
    "username": "acme_desk_bot"
  },
  "chat": {
    "title": "Acme team",
    "topics": true
  },
  "linkCode": null,
  "shareContact": false,
  "status": "ok",
  "lastError": null,
  "updatedAt": 1760000000000
}
type ChangeTelegramOptionsResponse = {
  connected: boolean;
  linked: boolean;
  bot: {
    name: string;
    username: string;
  } | null;
  chat: {
    title: string;
    topics: boolean;
  } | null;
  linkCode: string | null;
  shareContact: boolean;
  status: string | null;
  lastError: string | null;
  updatedAt: number | null;
};

Send a Telegram test

POST /live/telegram/test · scope settings:write

Request

curl -X POST "$HELPPUFF_URL/api/v1/live/telegram/test" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/live/telegram/test`, {
  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 SendTelegramTestResponse;

Response 200

{
  "ok": true
}
type SendTelegramTestResponse = {
  ok: boolean;
};

Errors

Status Code When
400 bad_request No chat is linked yet.

Disconnect Telegram

DELETE /live/telegram · scope settings:write

Parameter In Required Description
site query no The site (a key always uses its own).

Request

curl -X DELETE "$HELPPUFF_URL/api/v1/live/telegram" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/live/telegram`, {
  method: 'DELETE',
  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 DisconnectTelegramResponse;

Response 200

{
  "connected": false
}
type DisconnectTelegramResponse = {
  connected: boolean;
};

Clone this wiki locally