Repository navigation
API Reference Leads
The people who gave contact details: your CRM's contacts, with custom attributes and site-defined tags. Keyed by email per site. Part of the API reference: setup, shared types and errors are there.
-
List contact tags:
GET /contact-tags -
Create a contact tag:
POST /contact-tags -
Change a contact tag:
PATCH /contact-tags/:id -
Delete a contact tag:
DELETE /contact-tags/:id -
List leads:
GET /leads -
Create a lead:
POST /leads -
Get a lead:
GET /leads/:id -
Update a lead:
PATCH /leads/:id -
Delete a lead and everything about them:
DELETE /leads/:id -
Export leads as CSV:
GET /leads.csv
GET /contact-tags · scope leads:read
The site-defined tags that a contact may carry. Create, rename and remove definitions in Settings → Contact tags; assign them with PATCH /leads/:id.
| Parameter | In | Required | Description |
|---|---|---|---|
site |
query | no | The site (a key always uses its own). |
Request
curl "$HELPPUFF_URL/api/v1/contact-tags" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/contact-tags`, {
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 ListContactTagsResponse;Response 200
{
"tags": [
{
"id": "ctag_vip",
"name": "VIP",
"color": "#eab308"
}
],
"colors": [
"#6b7280",
"#ef4444",
"#f97316",
"#eab308",
"#22c55e",
"#14b8a6",
"#3b82f6",
"#8b5cf6",
"#ec4899"
]
}type ListContactTagsResponse = {
tags: Array<{
id: string;
name: string;
color: string;
}>;
colors: string[];
};POST /contact-tags · scope settings:write
Up to 100 per site.
| Body field | Required | Description |
|---|---|---|
name |
yes | Up to 40 characters, one per name (case does not matter). |
color |
no | A hex colour like #eab308; one is picked if left out. |
Request
curl -X POST "$HELPPUFF_URL/api/v1/contact-tags" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "VIP",
"color": "#eab308"
}'type CreateContactTagRequest = {
/** Up to 40 characters, one per name (case does not matter). */
name: string;
/** A hex colour like `#eab308`; one is picked if left out. */
color?: string;
};
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/contact-tags`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'VIP',
color: '#eab308'
} satisfies CreateContactTagRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as CreateContactTagResponse;Response 201
{
"id": "ctag_vip",
"name": "VIP",
"color": "#eab308"
}type CreateContactTagResponse = {
id: string;
name: string;
color: string;
};Errors
| Status | Code | When |
|---|---|---|
| 409 | conflict |
A contact tag with this name exists. |
PATCH /contact-tags/:id · scope settings:write
Renames or recolours it on every contact that has it.
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The record's id. |
| Body field | Required | Description |
|---|---|---|
name |
no | Up to 40 characters. |
color |
no | A hex colour. |
Request
curl -X PATCH "$HELPPUFF_URL/api/v1/contact-tags/ID" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Key account"
}'type ChangeContactTagRequest = {
/** Up to 40 characters. */
name?: string;
/** A hex colour. */
color?: string;
};
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/contact-tags/${id}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Key account'
} satisfies ChangeContactTagRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as ChangeContactTagResponse;Response 200
{
"id": "ctag_vip",
"name": "Key account",
"color": "#eab308"
}type ChangeContactTagResponse = {
id: string;
name: string;
color: string;
};DELETE /contact-tags/:id · scope settings:write
Removes it from every contact too.
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The record's id. |
Request
curl -X DELETE "$HELPPUFF_URL/api/v1/contact-tags/ID" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/contact-tags/${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 DeleteContactTagResponse;Response 200
{
"id": "ctag_vip",
"deleted": true
}type DeleteContactTagResponse = {
id: string;
deleted: boolean;
};GET /leads · scope leads:read
Most recently updated first, up to 500. Each contact includes its tags. Status and status counts remain for older integrations.
| Parameter | In | Required | Description |
|---|---|---|---|
q |
query | no | Search name, email, phone, company, notes and attributes. |
status |
query | no |
new, contacted, qualified, won or lost. |
tag |
query | no | A contact tag id, or untagged. Finds contacts with that tag; a contact may have several. |
site |
query | no | The site (a key always uses its own). |
Request
curl "$HELPPUFF_URL/api/v1/leads" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/leads`, {
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 ListLeadsResponse;Response 200
{
"items": [
{
"id": "lead_5e3f0c8a-1f7b-4f8e-9a51-0e2b7c4d1a90",
"site": "acme",
"conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
"name": "Ada Lovelace",
"email": "ada@example.com",
"phone": "0400 111 222",
"company": "Analytical Engines",
"address": null,
"fields": "{\"company\":\"Analytical Engines\"}",
"attributes": {
"plan": "pro"
},
"tags": [
{
"id": "ctag_vip",
"name": "VIP",
"color": "#eab308"
}
],
"source": "form",
"status": "new",
"notes": null,
"createdAt": 1760000000000,
"updatedAt": 1760000000000,
"conversations": 2,
"lastConversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
"openCallbacks": 1
}
],
"counts": {
"new": 1
}
}type ListLeadsResponse = {
items: Array<{
id: string;
site: string;
conversationId: string;
name: string;
email: string;
phone: string;
company: string;
address: string | null;
fields: string;
attributes: {
plan: string;
};
tags: Array<{
id: string;
name: string;
color: string;
}>;
source: string;
status: string;
notes: string | null;
createdAt: number;
updatedAt: number;
conversations: number;
lastConversationId: string;
openCallbacks: number;
}>;
counts: Record<string, number>;
};POST /leads · scope leads:write
Adds a contact from elsewhere (your CRM, an import). Needs a name, an email or a phone. One lead per email per site: a second answers 409 with the first one's id. Sends lead.captured with source: "api".
| Body field | Required | Description |
|---|---|---|
name |
no | Up to 200 characters. |
email |
no | One lead per email per site. |
phone |
no | Up to 40 characters. |
company |
no | Up to 200 characters. |
address |
no | Up to 500 characters. |
attributes |
no | Custom attributes: an object of strings (up to 50). |
fields |
no | Any other details: up to 20 string values. |
status |
no |
new (default), contacted, qualified, won or lost. |
notes |
no | Up to 5000 characters. |
Request
curl -X POST "$HELPPUFF_URL/api/v1/leads" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Grace Hopper",
"email": "grace@example.com",
"phone": "0400 333 444",
"company": "US Navy",
"attributes": {
"plan": "pro"
},
"status": "new",
"notes": "Met at the expo."
}'type CreateLeadRequest = {
/** Up to 200 characters. */
name?: string;
/** One lead per email per site. */
email?: string;
/** Up to 40 characters. */
phone?: string;
/** Up to 200 characters. */
company?: string;
/** Up to 500 characters. */
address?: string;
/** Custom attributes: an object of strings (up to 50). */
attributes?: {
plan?: string;
};
/** Any other details: up to 20 string values. */
fields?: string;
/** `new` (default), `contacted`, `qualified`, `won` or `lost`. */
status?: string;
/** Up to 5000 characters. */
notes?: string;
};
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/leads`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Grace Hopper',
email: 'grace@example.com',
phone: '0400 333 444',
company: 'US Navy',
attributes: {
plan: 'pro'
},
status: 'new',
notes: 'Met at the expo.'
} satisfies CreateLeadRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as CreateLeadResponse;Response 201
{
"id": "lead_7a1c…",
"site_id": "acme",
"conversation_id": null,
"name": "Grace Hopper",
"email": "grace@example.com",
"phone": "0400 333 444",
"fields": null,
"source": "api",
"status": "new",
"notes": "Met at the expo.",
"created_at": 1760000000000,
"updated_at": 1760000000000,
"company": "US Navy",
"address": null,
"attributes": {
"plan": "pro"
},
"tags": [
{
"id": "ctag_vip",
"name": "VIP",
"color": "#eab308"
}
]
}type CreateLeadResponse = {
id: string;
site_id: string;
conversation_id: string | null;
name: string;
email: string;
phone: string;
fields: string | null;
source: string;
status: string;
notes: string;
created_at: number;
updated_at: number;
company: string;
address: string | null;
attributes: {
plan: string;
};
tags: Array<{
id: string;
name: string;
color: string;
}>;
};Errors
| Status | Code | When |
|---|---|---|
| 409 | conflict |
A lead with this email exists (its id is in the message). |
GET /leads/:id · scope leads:read
The contact and its tags, with conversations (status, labels and attributes), the team's dated notes (teamNotes; notes is the contact's own notes field) and callback requests.
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The record's id. |
Request
curl "$HELPPUFF_URL/api/v1/leads/ID" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/leads/${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 GetLeadResponse;Response 200
{
"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"
}
],
"conversations": [
{
"id": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
"startedAt": 1760000000000,
"lastAt": 1760000120000,
"pageUrl": "https://acme.example/pricing",
"firstMessage": "How much is a blocked drain?",
"messageCount": 6,
"summary": null,
"intent": "Pricing question",
"channel": "widget",
"status": "closed",
"assignedTo": null,
"assignedName": null,
"labels": [
{
"id": "lbl_3f2a1b0c9d8e",
"name": "Urgent",
"color": "#ef4444"
}
],
"attributes": {
"orderId": "A-1042"
}
}
],
"teamNotes": [
{
"id": "note_mfx2k1a9b3c4",
"conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
"leadId": "lead_5e3f0c8a-1f7b-4f8e-9a51-0e2b7c4d1a90",
"author": "sam@acme.example",
"authorName": "Sam",
"text": "Called her back, booked for Tuesday.",
"createdAt": 1760000000000,
"updatedAt": 1760000000000
}
],
"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"
}
]
}type GetLeadResponse = {
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;
}>;
conversations: Array<{
id: string;
startedAt: number;
lastAt: number;
pageUrl: string;
firstMessage: string;
messageCount: number;
summary: string | null;
intent: string;
channel: string;
status: string;
assignedTo: string | null;
assignedName: string | null;
labels: Array<{
id: string;
name: string;
color: string;
}>;
attributes: {
orderId: string;
};
}>;
teamNotes: Array<{
id: string;
conversationId: string;
leadId: string;
author: string;
authorName: string;
text: string;
createdAt: number;
updatedAt: number;
}>;
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;
}>;
};PATCH /leads/:id · scope leads:write
Changes details, notes, custom attributes or tags. tags replaces the complete list of tag ids; attributes merges: sent keys are set, null removes one. The old status field remains for integrations. Sends lead.updated.
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The record's id. |
| Body field | Required | Description |
|---|---|---|
status |
no |
new, contacted, qualified, won or lost. |
name |
no | Up to 200 characters. |
email |
no | Changes it (still one lead per email per site). |
phone |
no | Up to 40 characters; empty or null clears it. |
company |
no | Up to 200 characters; empty or null clears it. |
address |
no | Up to 500 characters; empty or null clears it. |
attributes |
no | An object of strings; null removes a key. |
tags |
no | Complete list of contact tag ids from Settings → Contact tags; use [] to remove all tags. |
notes |
no | Replaces the notes field (up to 5000 characters). For a dated note, use POST /leads/:id/notes. |
Request
curl -X PATCH "$HELPPUFF_URL/api/v1/leads/ID" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"company": "Analytical Engines",
"tags": [],
"attributes": {
"plan": "pro",
"trial": null
}
}'type UpdateLeadRequest = {
/** `new`, `contacted`, `qualified`, `won` or `lost`. */
status?: string;
/** Up to 200 characters. */
name?: string;
/** Changes it (still one lead per email per site). */
email?: string;
/** Up to 40 characters; empty or null clears it. */
phone?: string;
/** Up to 200 characters; empty or null clears it. */
company?: string;
/** Up to 500 characters; empty or null clears it. */
address?: string;
/** An object of strings; `null` removes a key. */
attributes?: {
plan?: string;
trial?: string | null;
};
/**
* Complete list of contact tag ids from Settings → Contact tags; use `[]` to
* remove all tags.
*/
tags?: string[];
/**
* Replaces the notes field (up to 5000 characters). For a dated note, use
* `POST /leads/:id/notes`.
*/
notes?: string;
};
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/leads/${id}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
company: 'Analytical Engines',
tags: [],
attributes: {
plan: 'pro',
trial: null
}
} satisfies UpdateLeadRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as UpdateLeadResponse;Response 200
{
"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": "contacted",
"notes": null,
"created_at": 1760000000000,
"updated_at": 1760000000000,
"company": "Analytical Engines",
"address": null,
"attributes": {
"plan": "pro"
},
"tags": [
{
"id": "ctag_vip",
"name": "VIP",
"color": "#eab308"
}
]
}type UpdateLeadResponse = {
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;
}>;
};DELETE /leads/:id · scope leads:write
Deletes the contact and everything recorded about them: their conversations (messages, chat files, labels, notes), callback requests, notes, tags, jobs (with their history and files) and appointments; an upcoming appointment is cancelled at the synced calendar first. For a person who asks to be forgotten. Irreversible. erased counts what went.
| Parameter | In | Required | Description |
|---|---|---|---|
id |
path | yes | The record's id. |
Request
curl -X DELETE "$HELPPUFF_URL/api/v1/leads/ID" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/leads/${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 DeleteLeadAndEverythingAboutThemResponse;Response 200
{
"id": "lead_5e3f0c8a-1f7b-4f8e-9a51-0e2b7c4d1a90",
"deleted": true,
"conversationsDeleted": 2,
"erased": {
"conversations": 2,
"callbacks": 1,
"notes": 3,
"jobs": 1,
"appointments": 1,
"files": 2
}
}type DeleteLeadAndEverythingAboutThemResponse = {
id: string;
deleted: boolean;
conversationsDeleted: number;
erased: {
conversations: number;
callbacks: number;
notes: number;
jobs: number;
appointments: number;
files: number;
};
};GET /leads.csv · scope leads:read
Every lead, newest first, with a tags column. The status column remains for older integrations. Cells that could run as a spreadsheet formula are prefixed with '.
| Parameter | In | Required | Description |
|---|---|---|---|
site |
query | no | The site (a key always uses its own). |
Request
curl "$HELPPUFF_URL/api/v1/leads.csv" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/leads.csv`, {
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
},
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data: ExportLeadsAsCSVResponse = await response.text();Response 200
created,name,email,phone,company,address,status,source,notes,attributes,site,conversation,tags
2025-10-09T08:53:20.000Z,Ada Lovelace,ada@example.com,0400 111 222,Analytical Engines,,new,form,,"{""plan"":""pro""}",acme,1b0f6a52-…,/** CSV text. */
type ExportLeadsAsCSVResponse = string;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