Skip to content

Conversations API ​

Read, summarise and delete conversations, from the widget and the API alike. Part of the API reference: setup, shared types and errors are there.

List conversations ​

GET /conversations · scope conversations:read

Newest activity first, 30 at a time. Pass next from the answer as before for the next page.

ParameterInRequiredDescription
qquerynoSearch messages, summaries and lead details (up to 48 characters).
filterquerynoall, leads (with a lead), unsummarized, callbacks (with an open callback request) or waiting (a live chat waiting for the team's reply).
statusquerynoall, bot (the assistant has it), live (a person has it) or closed (closed, or quiet for live.closeAfterMinutes).
labelquerynoConversations with this label (its id or name).
assignedquerynome, none, or a team member's email.
externalIdquerynoConversations started over the API with this externalId.
beforequerynoCursor: the next of the previous page.
limitqueryno1–100, default 30.
sitequerynoThe site (a key always uses its own).

Request

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

Response 200

json
{
  "items": [
    {
      "id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
      "site": "acme",
      "startedAt": 1760000000000,
      "lastAt": 1760000120000,
      "pageUrl": "https://acme.example/pricing",
      "country": "AU",
      "firstMessage": "How much is a blocked drain?",
      "messageCount": 6,
      "summary": "Ada asked about a blocked drain and booked a callback.",
      "intent": "Pricing question",
      "leadName": "Ada Lovelace",
      "leadEmail": "ada@example.com",
      "leadPhone": "0400 111 222",
      "leadStatus": "new",
      "callback": "open",
      "status": "live",
      "assignedTo": "sam@acme.example",
      "assignedName": "Sam",
      "waitingSince": 1760000120000,
      "attributes": {
        "orderId": "A-1042"
      },
      "labels": [
        {
          "id": "lbl_3f2a1b0c9d8e",
          "name": "Urgent",
          "color": "#ef4444"
        }
      ]
    }
  ],
  "next": null
}
ts
type ListConversationsResponse = {
  items: Array<{
    id: string;
    site: string;
    startedAt: number;
    lastAt: number;
    pageUrl: string;
    country: string;
    firstMessage: string;
    messageCount: number;
    summary: string;
    intent: string;
    leadName: string;
    leadEmail: string;
    leadPhone: string;
    leadStatus: string;
    callback: string;
    status: string;
    assignedTo: string | null;
    assignedName: string | null;
    waitingSince: number | null;
    attributes: {
      orderId: string;
    };
    labels: Array<{
      id: string;
      name: string;
      color: string;
    }>;
  }>;
  next: string | null;
};

Get a conversation ​

GET /conversations/:id · scope conversations:read

The conversation (with its status, who has it, custom attributes, data: what the site's tools returned or saved, by tool name, and user: the signed-in visitor, verified, or null), its lead, callback requests, labels, the team's notes and every message both ways (a person's replies carry author).

ParameterInRequiredDescription
idpathyesThe record's id.

Request

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

Response 200

json
{
  "conversation": {
    "id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
    "site_id": "acme",
    "started_at": 1760000000000,
    "last_at": 1760000120000,
    "page_url": "https://acme.example/pricing",
    "page_title": "Pricing",
    "referrer": null,
    "utm": null,
    "locale": "en-AU",
    "country": "AU",
    "first_message": "How much is a blocked drain?",
    "message_count": 2,
    "lead_id": "lead_5e3f0c8a-1f7b-4f8e-9a51-0e2b7c4d1a90",
    "summary": null,
    "intent": null,
    "summarized_at": null,
    "completed_at": null,
    "ended_at": null,
    "channel": "widget",
    "status": "bot",
    "assigned_to": null,
    "assigned_name": null,
    "handover_at": null,
    "waiting_since": null,
    "closed_at": null,
    "attributes": {
      "orderId": "A-1042"
    },
    "data": {
      "order_status": {
        "status": "shipped",
        "delivery": {
          "date": "2026-10-12"
        }
      }
    },
    "user": {
      "id": "u_8812",
      "email": "ada@example.com",
      "plan": "pro"
    }
  },
  "lead": {
    "id": "lead_5e3f0c8a-1f7b-4f8e-9a51-0e2b7c4d1a90",
    "site_id": "acme",
    "conversation_id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
    "name": "Ada Lovelace",
    "email": "ada@example.com",
    "phone": "0400 111 222",
    "fields": "{\"company\":\"Analytical Engines\"}",
    "source": "form",
    "status": "new",
    "notes": null,
    "created_at": 1760000000000,
    "updated_at": 1760000000000,
    "company": "Analytical Engines",
    "address": null,
    "attributes": {
      "plan": "pro"
    },
    "tags": [
      {
        "id": "ctag_vip",
        "name": "VIP",
        "color": "#eab308"
      }
    ]
  },
  "callbacks": [
    {
      "id": "cb_mfx2k1a9b3c",
      "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
      "leadId": "lead_5e3f0c8a-1f7b-4f8e-9a51-0e2b7c4d1a90",
      "name": "Ada Lovelace",
      "phone": "0400 111 222",
      "email": "ada@example.com",
      "reason": "A quote for two rooms",
      "status": "open",
      "note": null,
      "requestedAt": 1760000000000,
      "closedAt": null,
      "closedBy": null,
      "pageUrl": "https://acme.example/pricing"
    }
  ],
  "labels": [
    {
      "id": "lbl_3f2a1b0c9d8e",
      "name": "Urgent",
      "color": "#ef4444",
      "addedBy": "ai",
      "addedAt": 1760000000000
    }
  ],
  "notes": [
    {
      "id": "note_mfx2k1a9b3c4",
      "author": "sam@acme.example",
      "authorName": "Sam",
      "text": "Called her back, booked for Tuesday.",
      "createdAt": 1760000000000,
      "updatedAt": 1760000000000
    }
  ],
  "messages": [
    {
      "id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10:u1",
      "role": "user",
      "type": "text",
      "text": "How much is a blocked drain?",
      "payload": null,
      "ts": 1760000000000,
      "feedback": null,
      "author": null
    },
    {
      "id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10:m_mfx2k1",
      "role": "agent",
      "type": "text",
      "text": "Usually $180–$250.",
      "payload": {},
      "ts": 1760000000001,
      "feedback": 1,
      "author": null
    }
  ]
}
ts
type GetConversationResponse = {
  conversation: {
    id: string;
    site_id: string;
    started_at: number;
    last_at: number;
    page_url: string;
    page_title: string;
    referrer: string | null;
    utm: string | null;
    locale: string;
    country: string;
    first_message: string;
    message_count: number;
    lead_id: string;
    summary: string | null;
    intent: string | null;
    summarized_at: number | null;
    completed_at: number | null;
    ended_at: number | null;
    channel: string;
    status: string;
    assigned_to: string | null;
    assigned_name: string | null;
    handover_at: number | null;
    waiting_since: string | null;
    closed_at: number | null;
    attributes: {
      orderId: string;
    };
    data: {
      order_status: {
        status: string;
        delivery: {
          date: string;
        };
      };
    };
    user: {
      id: string;
      email: string;
      plan: string;
    } | null;
  };
  lead: {
    id: string;
    site_id: string;
    conversation_id: string;
    name: string;
    email: string;
    phone: string;
    fields: string;
    source: string;
    status: string;
    notes: string | null;
    created_at: number;
    updated_at: number;
    company: string;
    address: string | null;
    attributes: {
      plan: string;
    };
    tags: Array<{
      id: string;
      name: string;
      color: string;
    }>;
  };
  callbacks: Array<{
    id: string;
    conversationId: string;
    leadId: string;
    name: string;
    phone: string;
    email: string;
    reason: string;
    status: string;
    note: string | null;
    requestedAt: number;
    closedAt: number | null;
    closedBy: string | null;
    pageUrl: string;
  }>;
  labels: Array<{
    id: string;
    name: string;
    color: string;
    addedBy: string;
    addedAt: number;
  }>;
  notes: Array<{
    id: string;
    author: string;
    authorName: string;
    text: string;
    createdAt: number;
    updatedAt: number;
  }>;
  messages: Message[];
};

Label a conversation, or set its attributes ​

PATCH /conversations/:id · scope conversations:write

Custom attributes are key-value strings for your own data (an order number, a plan): sent keys are set, null removes one, others stay; up to 50. Labels come from the site's list (GET /labels), by id or name: labels replaces them, addLabels and removeLabels change them.

ParameterInRequiredDescription
idpathyesThe record's id.
Body fieldRequiredDescription
attributesnoAn object of strings; null removes a key. Keys: up to 64 letters, digits, spaces, _ - .; values up to 1000 characters.
labelsnoThe labels it should have (ids or names), replacing the ones it has.
addLabelsnoLabels to add.
removeLabelsnoLabels to remove.

Request

bash
curl -X PATCH "$HELPPUFF_URL/api/v1/conversations/ID" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "attributes": {
    "orderId": "A-1042",
    "coupon": null
  },
  "addLabels": [
    "Urgent"
  ]
}'
ts
type LabelConversationSetItsAttributesRequest = {
  /**
   * An object of strings; `null` removes a key. Keys: up to 64 letters,
   * digits, spaces, `_ - .`; values up to 1000 characters.
   */
  attributes?: {
    orderId?: string;
    coupon?: string | null;
  };
  /** The labels it should have (ids or names), replacing the ones it has. */
  labels?: string;
  /** Labels to add. */
  addLabels?: string[];
  /** Labels to remove. */
  removeLabels?: string;
};

const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/conversations/${id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    attributes: {
      orderId: 'A-1042',
      coupon: null
    },
    addLabels: [
      'Urgent'
    ]
  } satisfies LabelConversationSetItsAttributesRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as LabelConversationSetItsAttributesResponse;

Response 200

json
{
  "id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "labels": [
    {
      "id": "lbl_3f2a1b0c9d8e",
      "name": "Urgent",
      "color": "#ef4444",
      "addedBy": "key:k7m3p9q2r4s8",
      "addedAt": 1760000000000
    }
  ],
  "attributes": {
    "orderId": "A-1042"
  },
  "data": {},
  "notes": []
}
ts
type LabelConversationSetItsAttributesResponse = {
  id: string;
  labels: Array<{
    id: string;
    name: string;
    color: string;
    addedBy: string;
    addedAt: number;
  }>;
  attributes: {
    orderId: string;
  };
  data: Record<string, unknown>;
  notes: string[];
};

Errors

StatusCodeWhen
400bad_requestAn unknown label, a bad attribute key or value, or more than 50 attributes.

Summarise a conversation ​

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

Writes (or rewrites) its AI summary and labels, and sends conversation.summarized. Uses Workers AI (a few neurons). When autoSummary is on, conversations are also summarised automatically 5 minutes after they go quiet.

ParameterInRequiredDescription
idpathyesThe record's id.

Request

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

Response 200

json
{
  "summary": "Ada asked about a blocked drain and booked a callback for tomorrow morning.",
  "intent": "Blocked drain",
  "sentiment": "positive",
  "leadQuality": "hot",
  "outcome": "callback_requested",
  "topics": [
    "drains",
    "pricing"
  ],
  "unanswered": [],
  "followUp": "Call Ada tomorrow before 10am.",
  "lead": {
    "name": "Ada Lovelace",
    "phone": "0400 111 222"
  }
}
ts
type SummariseConversationResponse = {
  summary: string;
  intent: string;
  sentiment: string;
  leadQuality: string;
  outcome: string;
  topics: string[];
  unanswered: string[];
  followUp: string;
  lead: {
    name: string;
    phone: string;
  };
};

Delete a conversation ​

DELETE /conversations/:id · scope conversations:write

Deletes it with its messages and callback requests. The lead it produced stays (delete it with DELETE /leads/:id).

ParameterInRequiredDescription
idpathyesThe record's id.

Request

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

Response 200

json
{
  "id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "deleted": true
}
ts
type DeleteConversationResponse = {
  id: string;
  deleted: boolean;
};

MIT licensed. Runs in your own Cloudflare account.