Skip to content

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.

ParameterInRequiredDescription
sitequerynoThe site (a key always uses its own).

Request

bash
curl "$HELPPUFF_URL/api/v1/live/status" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

json
{
  "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
}
ts
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.

ParameterInRequiredDescription
idpathyesThe record's id.
Body fieldRequiredDescription
textyesUp to 4000 characters.

Request

bash
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?"
}'
ts
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

json
{
  "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"
  }
}
ts
type ReplyInLiveChatResponse = {
  id: string;
  ts: number;
  role: string;
  type: string;
  text: string;
  meta: {
    human: boolean;
    agentName: string;
  };
};

Errors

StatusCodeWhen
400bad_requestLive 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.

ParameterInRequiredDescription
idpathyesThe record's id.

Request

bash
curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/takeover" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

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

Errors

StatusCodeWhen
400bad_requestLive 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.

ParameterInRequiredDescription
idpathyesThe record's id.
Body fieldRequiredDescription
toyes"me", an email on the team, or null.

Request

bash
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"
}'
ts
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

json
{
  "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "assignedTo": "sam@acme.example",
  "assignedName": "Sam"
}
ts
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.

ParameterInRequiredDescription
idpathyesThe record's id.

Request

bash
curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/close" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

json
{
  "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "status": "closed",
  "closedAt": 1760000000000
}
ts
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.

ParameterInRequiredDescription
idpathyesThe record's id.

Request

bash
curl -X POST "$HELPPUFF_URL/api/v1/conversations/ID/handback" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

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

Errors

StatusCodeWhen
409conflictThe 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>.

ParameterInRequiredDescription
sitequerynoThe site (a key always uses its own).

Request

bash
curl "$HELPPUFF_URL/api/v1/live/telegram" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

json
{
  "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
}
ts
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 fieldRequiredDescription
tokenyesThe bot token from @BotFather.

Request

bash
curl -X POST "$HELPPUFF_URL/api/v1/live/telegram" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "token": "7123456789:AAH3k…"
}'
ts
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

json
{
  "connected": true,
  "linked": false,
  "bot": {
    "name": "Acme Desk",
    "username": "acme_desk_bot"
  },
  "chat": null,
  "linkCode": "9f3a1c2e",
  "shareContact": true,
  "status": "linking",
  "lastError": null,
  "updatedAt": 1760000000000
}
ts
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

StatusCodeWhen
400bad_requestTelegram refused the token, or the webhook.

Change Telegram options ​

PATCH /live/telegram · scope settings:write

Body fieldRequiredDescription
shareContactnoInclude visitors' email and phone in what goes to Telegram.

Request

bash
curl -X PATCH "$HELPPUFF_URL/api/v1/live/telegram" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "shareContact": false
}'
ts
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

json
{
  "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
}
ts
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

bash
curl -X POST "$HELPPUFF_URL/api/v1/live/telegram/test" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

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

Errors

StatusCodeWhen
400bad_requestNo chat is linked yet.

Disconnect Telegram ​

DELETE /live/telegram · scope settings:write

ParameterInRequiredDescription
sitequerynoThe site (a key always uses its own).

Request

bash
curl -X DELETE "$HELPPUFF_URL/api/v1/live/telegram" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

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

MIT licensed. Runs in your own Cloudflare account.