Skip to content

API Reference Leads

github-actions[bot] edited this page Oct 17, 2026 · 4 revisions

Leads API

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 · 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[];
};

Create a contact tag

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.

Change a contact tag

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 a contact tag

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;
};

List leads

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>;
};

Create a lead

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 a lead

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;
  }>;
};

Update a lead

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 a lead and everything about them

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;
  };
};

Export leads as CSV

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;

Clone this wiki locally