Skip to content

Leads

A lead is one person your AI setter talks to. The API exposes the same record as the Leads tab of the dashboard: status, tags, owner, next action, deal outcome and notes.

Method Path Scope
GET /leads leads:read
GET /leads/{id} leads:read
PATCH /leads/{id} leads:write
GET /leads/{id}/notes leads:read
POST /leads/{id}/notes leads:write
POST /leads/{id}/handoff leads:write
POST /leads/{id}/resume leads:write

Leads come back most recently active first, 25 per page by default.

Terminal window
curl "https://app.whatsetter.com/api/v1/leads?status=qualified&campaign_id=acme-france-1-ab12&limit=25" \
-H "Authorization: Bearer ws_live_…"
{
"data": [
{
"id": "66f1a2b3c4d5e6f7a8b9c0d1",
"phone": "33612345678",
"whatsapp_id": "33612345678@c.us",
"name": "Julien",
"status": "qualified",
"qualified": true,
"qualified_at": "2026-09-12T10:04:31.000Z",
"replied": true,
"campaign_id": "acme-france-1-ab12",
"list_id": null,
"channel": "whatsapp",
"ig_username": null,
"external_reference": "4471",
"source": "api",
"source_confidence": null,
"tracked_link_id": null,
"email": "julien@example.com",
"tags": ["hot", "demo"],
"messages_count": 14,
"booking_status": "booked",
"first_contact_at": "2026-09-10T08:00:12.000Z",
"last_message_at": "2026-09-18T16:42:05.000Z",
"last_message_direction": "inbound",
"assigned_to": "66a1b2c3d4e5f6a7b8c9d0e1",
"stage": "booked",
"deal_status": "open",
"lost_reason": null,
"next_action_at": "2026-09-22T09:00:00.000Z",
"next_action_label": "Call back",
"created_at": "2026-09-10T07:59:58.000Z",
"updated_at": "2026-09-18T16:42:05.000Z"
}
],
"pagination": { "has_more": true, "next_cursor": "66f1a2b3c4d5e6f7a8b9c0d1" }
}

phone is digits only, without the +. Pass cursor=next_cursor to get the next page.

Parameter What it does
status One of the statuses below. qualified returns every lead that was qualified at some point (its qualification date is set).
campaign_id The campaign’s campaign_id slug or its id, both from GET /campaigns.
phone Exact lookup, international format (+33612345678).
external_reference Exact lookup on the id you sent with POST /campaigns/{id}/leads.
source Where the lead came from: instagram, facebook, tiktok, youtube, linkedin, x, snapchat, pinterest, google, website, email, sms, qr, ads, other, direct, list, api, webhook, inbound, manual.
tracked_link_id Only leads brought by this tracked link.
channel whatsapp or instagram.
since Only leads active after this ISO 8601 date (on last_message_at).
limit, cursor Pagination, 1 to 100 per page.
status Meaning
queued Imported, waiting for its first message.
pending The agent stopped and waits for your team.
in_progress The conversation is underway.
qualified The agent validated interest, or you set it from the API.
not_interested Wrong fit or explicit refusal.
cold No reply after the follow-ups.
stopped The conversation was stopped.
unknown An internal value the public API does not name.
Terminal window
curl "https://app.whatsetter.com/api/v1/leads/66f1a2b3c4d5e6f7a8b9c0d1" \
-H "Authorization: Bearer ws_live_…"

The response is { "data": lead } with the same object as above. A lead of another workspace answers 404, never 403.

Send only the fields you change. At least one is required.

Terminal window
curl -X PATCH "https://app.whatsetter.com/api/v1/leads/66f1a2b3c4d5e6f7a8b9c0d1" \
-H "Authorization: Bearer ws_live_…" \
-H "Content-Type: application/json" \
-d '{
"status": "qualified",
"tags": ["hot", "demo"],
"assigned_to": "66a1b2c3d4e5f6a7b8c9d0e1",
"next_action_at": "2026-09-22T09:00:00.000Z",
"next_action_label": "Call back"
}'

The response is the updated lead, 200.

Field Rules
status qualified, in_progress, not_interested, cold, pending, stopped or queued. Setting qualified stamps qualified_at once.
tags Array of up to 20 strings, 40 characters each. Replaces the whole list. Empty or too long tags are dropped, duplicates removed.
assigned_to A member id from GET /team/members, or null to unassign. Unknown id: 422.
next_action_at ISO 8601 date, or null.
next_action_label Up to 80 characters, or null. Needs a next_action_at date (422 otherwise). Clearing the date clears the label.
deal_status open, won or lost.
lost_reason One of price, timing, unqualified, competitor, unreachable, other. Required with deal_status: "lost", cleared for open and won.

Every change writes the lead’s journal, the same events the dashboard writes (owner, next_action, outcome), with your API key’s name as the actor.

Notes are the free-text entries your team sees on the lead’s record.

Terminal window
curl -X POST "https://app.whatsetter.com/api/v1/leads/66f1a2b3c4d5e6f7a8b9c0d1/notes" \
-H "Authorization: Bearer ws_live_…" \
-H "Content-Type: application/json" \
-d '{"text":"Called back, wants a quote before Friday."}'
{
"data": {
"id": "66f1a2b3c4d5e6f7a8b9c0e2",
"text": "Called back, wants a quote before Friday.",
"author": "API · My CRM",
"created_at": "2026-09-19T08:12:00.000Z"
}
}

text is 1 to 2000 characters. The author is API · <key name> for notes you create, or the teammate’s name for notes written in the dashboard. GET /leads/{id}/notes returns { "data": [ ...notes ] }, newest first, up to 100.

Hand a conversation to a human, then give it back

Section titled “Hand a conversation to a human, then give it back”

POST /leads/{id}/handoff stops the AI agent on this lead. It stays silent until you call POST /leads/{id}/resume. This is the same switch as the dashboard’s take-over toggle, so the two never disagree.

Terminal window
curl -X POST "https://app.whatsetter.com/api/v1/leads/66f1a2b3c4d5e6f7a8b9c0d1/handoff" \
-H "Authorization: Bearer ws_live_…"
{
"data": {
"id": "66f1a2b3c4d5e6f7a8b9c0d1",
"external_reference": "4471",
"agent_active": false,
"manual_override": true,
"status": "in_progress",
"dashboard_synced": true,
"updated_at": "2026-09-19T08:12:00.000Z"
}
}
  • The lead’s status is preserved: handing over never marks a lead as stopped or qualified.
  • Both calls are idempotent: a second handoff on a lead already handed over answers 200.
  • dashboard_synced: false means the agent is stopped but the Leads list may lag. The conversation view is always right.
  • The status rule: handoff and resume only apply to leads the agent has already engaged (in_progress, stopped, cold, qualified, not_interested). A queued or pending lead has no conversation to hand over and answers 409 invalid_state.

stage is read-only and derived from the lead exactly like the dashboard’s funnel. It answers “where is this person commercially?” in one word, while status is the AI’s own vocabulary.

stage When
contacted Written to, no answer yet.
conversation The lead replied, the AI or a human is talking.
waiting The AI stopped and waits for your team (status: pending).
qualified Interest validated (status: qualified).
booked A meeting is on the calendar, or was held. Wins over every other value.
not_interested Wrong fit or refusal.
cold No answer after the follow-ups.
stopped The conversation was stopped.

Send your own record id as external_reference when you push an opt-in lead with POST /campaigns/{id}/leads. WhatSetter stores it verbatim, returns it on every lead and every webhook, and lets you look it up:

Terminal window
curl "https://app.whatsetter.com/api/v1/leads?external_reference=4471" \
-H "Authorization: Bearer ws_live_…"

external_reference is null for leads that did not come from you (CSV imports, list imports, sales-page forms). For those, match on phone: digits only on both sides.

HTTP code When
401 missing_api_key, invalid_api_key Key absent, unknown or revoked.
403 insufficient_scope The key lacks leads:read or leads:write.
404 not_found Unknown lead, or a lead of another workspace.
409 invalid_state Handoff or resume on a lead that was never contacted.
422 validation_error Unknown status, bad phone, unknown member id, empty body, invalid or expired cursor.
429 rate_limited Too many requests this minute. Honor Retry-After.
502 upstream_error The agent-control workflow was unreachable. Safe to retry.