Leads
From the Campaigns API reference. Requests use the base URL https://app.warmerly.com/api/v1 and an API key, see Authentication.
#List leads
GET /campaigns/{id}/leads| Query param | Default | Max |
|---|---|---|
limit | 200 | 1000 |
offset | 0 | , |
status | all | One lead status; an unknown value is ignored |
q | none | Search email, name or company |
sort | newest | newest, oldest or name |
curl "https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads?limit=50" \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{
"leads": [
{
"id": "11111111-2222-4333-8444-555555555555",
"campaignId": "b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f",
"leadId": null,
"email": "jane@acme.com",
"firstName": "Jane",
"lastName": "Doe",
"companyName": "Acme Inc",
"linkedinProfileId": null,
"linkedinProfileUrl": null,
"linkedinName": null,
"linkedinProfilePictureUrl": null,
"linkedinHeadline": null,
"linkedinCompany": null,
"phone": null,
"linkedinPostsCache": null,
"customVars": { "role": "Head of Growth" },
"status": "queued",
"currentStep": 0,
"nextRunAt": null,
"repliedAt": null,
"bouncedAt": null,
"unsubscribedAt": null,
"createdAt": "2026-06-15T12:00:00.000Z",
"sentCount": 1,
"lastSentAt": "2026-06-16T09:00:00.000Z",
"openedCount": 0,
"lastRepliedAt": null
}
],
"total": 240,
"offset": 0,
"limit": 50,
"counts": { "all": 240, "queued": 168, "active": 21 },
"research": { "researchable": 240, "researched": 12 }
}status is one of queued, active, completed, replied, bounced, unsubscribed, skipped. Each lead is the stored row (so it also has fields such as researchUrl, country and verificationStatus) plus sentCount, lastSentAt, openedCount and lastRepliedAt. total is the count for the current filter, counts is per status across the whole campaign, and research counts leads that can be researched and leads that already have a website summary.
#Add leads
POST /campaigns/{id}/leadsBulk-insert 1 to 10,000 leads at once. Leads are deduped by lowercased email within the request and against existing leads in the campaign (ON CONFLICT DO NOTHING on (campaignId, email)), duplicates are silently skipped, not errored. Addresses that a free verification check proves invalid are not added and are counted in rejected.
| Field | Type | Required |
|---|---|---|
leads[].email | string (valid email) | yes |
leads[].firstName | string or null | no |
leads[].lastName | string or null | no |
leads[].companyName | string or null | no |
leads[].customVars | object of string → string | no |
leads[].researchUrl | string (URL) or null | no |
leads[].country | string (up to 8 characters) or null | no |
leads[].region | string (up to 120 characters) or null | no |
curl -X POST https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{ "email": "jane@acme.com", "firstName": "Jane", "lastName": "Doe", "companyName": "Acme Inc", "customVars": { "role": "Head of Growth" } },
{ "email": "bob@widgets.io", "firstName": "Bob" }
]
}'{ "added": 2, "skipped": 0, "rejected": 0 }A billing-locked workspace returns 402 billing_locked. Going over your plan's leads-per-campaign ceiling refuses the whole request with 402 and code campaign_lead_limit.
#Remove leads
DELETE /campaigns/{id}/leadsWith no body, deletes every lead attached to the campaign. Send { "leadIds": ["..."] } (1 to 10,000 UUIDs) to delete only those leads. A single lead can also be removed with DELETE /campaigns/{id}/leads/{leadId}, which returns { "ok": true } (or 404 not_found).
curl -X DELETE https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{ "deleted": 240 }#Update a lead's email
PATCH /campaigns/{id}/leads/{leadId}Send at least one of email (a valid address, saved lowercased), researchUrl (a URL up to 2048 characters, or null) or websiteSummary (text up to 4000 characters, replaces the lead's website research summary). email is the usual use: fixing a bad address found via Find emails or manual correction.
curl -X PATCH https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads/11111111-2222-4333-8444-555555555555 \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "email": "jane.doe@acme.com" }'{ "lead": { "id": "11111111-2222-4333-8444-555555555555", "email": "jane.doe@acme.com", "...": "..." } }If another lead in the same campaign already has that email, returns 400 bad_request ("Another lead in this campaign already has that email").
#Import LinkedIn leads
POST /campaigns/{id}/leads/import-linkedinBulk-imports leads from LinkedIn profile data (e.g. a Sales Navigator search or connections list). Dedupes against existing leads by linkedinProfileId. If the campaign already has at least one email sender attached, it immediately attempts to find email addresses for imported leads that have a company_name (best-effort, synchronous). Each lookup uses your email lookup allowance; when it runs short the import still succeeds and the remaining leads are counted in emailLookupsSkipped.
| Field | Type | Required | |
|---|---|---|---|
leads[].id | string | yes | LinkedIn provider ID (public_identifier's underlying id) |
leads[].public_identifier | string | yes | |
leads[].name | string | yes | Split into firstName/lastName |
leads[].headline | string or null | no | |
leads[].location | string or null | no | Accepted but not stored |
leads[].profile_picture_url | string or null | no | |
leads[].company_name | string or null | no | Used for the email lookup |
leads[].profile_url | string | yes |
1 to 1,000 leads per request. Going over your plan's leads-per-campaign ceiling refuses the request with 402 and code campaign_lead_limit.
curl -X POST https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads/import-linkedin \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{
"id": "ACoAAA1234567",
"public_identifier": "jane-doe-1234",
"name": "Jane Doe",
"headline": "Head of Growth at Acme",
"profile_picture_url": "https://media.licdn.com/...",
"company_name": "Acme Inc",
"profile_url": "https://www.linkedin.com/in/jane-doe-1234"
}
]
}'{ "imported": 1, "skipped": 0, "emailsFound": 1, "emailLookupsSkipped": 0 }#Find emails for LinkedIn leads
POST /campaigns/{id}/leads/find-emailsKicks off email lookups for every lead in the campaign that has a linkedinProfileUrl, no email and no earlier lookup attempt, then returns immediately, lookups continue running server-side after the response, subject to your plan's lookup quota.
curl -X POST https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads/find-emails \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"{ "started": 47, "skipped": 0 }skipped is nonzero if your remaining lookup quota is smaller than the number of eligible leads, only the leads that fit in the remaining quota are started. If your quota is fully exhausted, returns 400 bad_request with the quota details in error.details. With no eligible leads it returns { "started": 0, "skipped": 0 }.