GET/campaigns/:campaignId/leads
Credits: 0
LinkedIn risk: None
Rate limit: Global only
Purpose
Paginated list of leads in a campaign. Essential for MCP and polling when webhooks are not enough. Responses are an allowlisted CRM shape only — never a raw lead dump.
Request
GET /campaigns/search_abc/leads?tag=Interested&sort=taggedAt&limit=50
X-API-Key: sk_live_...
| Query | Notes |
|---|---|
tag | Optional exact tag filter (see PATCH …/leads/:leadId) |
replied | Optional boolean |
needsReply | Optional — leads waiting on a reply |
excluded | Optional — only true is supported (lists excluded leads). Any other value (including false) → 400 validation_error. Cleared leads are omitted from this filter. |
sort | Optional; always desc. Default createdAt. See matrix below |
limit | Optional; default 50, max 100 |
cursor | Opaque pagination cursor from previous response. Unknown, deleted, malformed, missing the active sort field, or not matching the active tag / replied / needsReply / excluded filter → 400 validation_error (does not silently restart at page 1) |
Filter rule: use at most one of tag, replied, needsReply, excluded per request. Combining them returns 400 validation_error.
sort matrix
sort | Allowed with |
|---|---|
createdAt (default) | any / none |
taggedAt | requires tag=… |
repliedAt | requires replied=true|false |
lastInboundMessageAt | none, or replied=true|false (not with tag) |
needsReply and excluded=true only support sort=createdAt. Invalid combo → 400 with fields: ['sort'].
Missing fields: leads missing the active sort field are omitted from that sort. Old leads without taggedAt / repliedAt / lastInboundMessageAt will not appear in those sorts. Prefer GET /activity?type=reply_received for “last reply.” createdAt is a sort key only — it is not returned on each lead object.
Changing sort or filters mid-pagination requires a fresh cursor.
Response 200 (illustrative)
{
"leads": [
{
"id": "lead_xyz",
"fullName": "Jane Doe",
"firstName": "Jane",
"lastName": "Doe",
"linkedinUrl": "https://www.linkedin.com/in/jane-doe/",
"jobTitle": "VP Sales",
"location": "London, UK",
"companyName": "Acme",
"companyLinkedin": "https://www.linkedin.com/company/acme/",
"website": "https://acme.com",
"email": "jane@acme.com",
"tag": "Interested",
"replied": true,
"needsReply": true,
"excluded": false,
"stage": "replied",
"repliedAt": "2026-03-01T12:00:00.000Z",
"lastInboundMessageAt": "2026-03-02T15:30:00.000Z",
"taggedAt": "2026-03-02T15:31:00.000Z"
}
],
"nextCursor": null
}
Name and contact fields
| Field | Notes |
|---|---|
fullName | Falls back to firstName + lastName when the stored value is empty — file imports often map only the separate name columns. null when there is nothing to compose from |
firstName, lastName | Stored name parts; null when unset |
location | Also present on get-lead |
companyLinkedin, website, email | The customer's own lead data, populated by file import or enrichment. Search-sourced leads are created with these blank, so expect null there |
excluded | true when the lead is excluded from outreach; otherwise false |
Timestamps (ISO or null)
| Field | Notes |
|---|---|
repliedAt | Updated when the lead is marked replied (may move on later inbound writes — not a frozen “first reply only” stamp) |
lastInboundMessageAt | Best lead-level signal for recent inbound content when present |
taggedAt | When the current tag was set; null on historical/seed rows |
For “when was my last reply?” prefer activity type=reply_received.
All string fields are trimmed and normalise empty to null, never "".
A lead's email is returned because it is the customer's own CRM data; this is distinct from an account owner's email, which is never exposed. Treat the response as personal data for GDPR purposes.
Prefer a small public stage enum:
new · connection_sent · connected · messaged · replied · tagged · excluded · failed · handed_off
Allowlisted fields only — never drafts, message logs, or provider ids. See get-lead for the slightly richer single-lead DTO.
Errors
error.code | When |
|---|---|
unauthorized | Missing/invalid API key |
not_found | Unknown campaign |
validation_error | Bad filter combo, excluded≠true, sort, or cursor |
rate_limited | Global RPM exceeded |
Good for
- “Show me Interested leads”
- “Show excluded leads” →
excluded=true - MCP
list_leads
MCP
Tool: list_leads (same single-filter + sort rules). Pass excluded: true only — never false.
See also
- Get lead
- GET /activity (
type=reply_received) - GET /insights/overview
- Batch update — tag / exclude many leads
- Batch delete — permanent remove