API Reference Conversations - knowtific/helppuff GitHub Wiki

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.

Parameter In Required Description
q query no Search messages, summaries and lead details (up to 48 characters).
filter query no all, leads (with a lead), unsummarized, callbacks (with an open callback request) or waiting (a live chat waiting for the team's reply).
status query no all, bot (the assistant has it), live (a person has it) or closed (closed, or quiet for live.closeAfterMinutes).
label query no Conversations with this label (its id or name).
assigned query no me, none, or a team member's email.
externalId query no Conversations started over the API with this externalId.
before query no Cursor: the next of the previous page.
limit query no 1–100, default 30.
site query no The site (a key always uses its own).

Request

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

{
  "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": "[email protected]",
      "leadPhone": "0400 111 222",
      "leadStatus": "new",
      "callback": "open",
      "status": "live",
      "assignedTo": "[email protected]",
      "assignedName": "Sam",
      "waitingSince": 1760000120000,
      "attributes": {
        "orderId": "A-1042"
      },
      "labels": [
        {
          "id": "lbl_3f2a1b0c9d8e",
          "name": "Urgent",
          "color": "#ef4444"
        }
      ]
    }
  ],
  "next": null
}
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).

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

Request

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

{
  "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": "[email protected]",
      "plan": "pro"
    }
  },
  "lead": {
    "id": "lead_5e3f0c8a-1f7b-4f8e-9a51-0e2b7c4d1a90",
    "site_id": "acme",
    "conversation_id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
    "name": "Ada Lovelace",
    "email": "[email protected]",
    "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": "[email protected]",
      "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": "[email protected]",
      "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
    }
  ]
}
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.

Parameter In Required Description
id path yes The record's id.
Body field Required Description
attributes no An object of strings; null removes a key. Keys: up to 64 letters, digits, spaces, _ - .; values up to 1000 characters.
labels no The labels it should have (ids or names), replacing the ones it has.
addLabels no Labels to add.
removeLabels no Labels to remove.

Request

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"
  ]
}'
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

{
  "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": []
}
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

Status Code When
400 bad_request An 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.

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

Request

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

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

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

Request

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

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