Less boo. More booked. Halloween offer: 30 days free on any paid plan. Claim 30 days freeEvery new account gets one free mailbox for warmup and replies. No domain to buy. See how it works

Skip to content

Leads

API referenceCampaigns

From the Campaigns API reference. Requests use the base URL https://app.warmerly.com/api/v1 and an API key, see Authentication.

API reference

#List leads

text
GET /campaigns/{id}/leads
Query paramDefaultMax
limit2001000
offset0,
statusallOne lead status; an unknown value is ignored
qnoneSearch email, name or company
sortnewestnewest, oldest or name
bash
curl "https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads?limit=50" \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{
  "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.

API reference

#Add leads

text
POST /campaigns/{id}/leads

Bulk-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.

FieldTypeRequired
leads[].emailstring (valid email)yes
leads[].firstNamestring or nullno
leads[].lastNamestring or nullno
leads[].companyNamestring or nullno
leads[].customVarsobject of string → stringno
leads[].researchUrlstring (URL) or nullno
leads[].countrystring (up to 8 characters) or nullno
leads[].regionstring (up to 120 characters) or nullno
bash
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" }
    ]
  }'
json
{ "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.

API reference

#Remove leads

text
DELETE /campaigns/{id}/leads

With 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).

bash
curl -X DELETE https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{ "deleted": 240 }
API reference

#Update a lead's email

text
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.

bash
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" }'
json
{ "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").

API reference

#Import LinkedIn leads

text
POST /campaigns/{id}/leads/import-linkedin

Bulk-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.

FieldTypeRequired
leads[].idstringyesLinkedIn provider ID (public_identifier's underlying id)
leads[].public_identifierstringyes
leads[].namestringyesSplit into firstName/lastName
leads[].headlinestring or nullno
leads[].locationstring or nullnoAccepted but not stored
leads[].profile_picture_urlstring or nullno
leads[].company_namestring or nullnoUsed for the email lookup
leads[].profile_urlstringyes

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.

bash
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"
      }
    ]
  }'
json
{ "imported": 1, "skipped": 0, "emailsFound": 1, "emailLookupsSkipped": 0 }
API reference

#Find emails for LinkedIn leads

text
POST /campaigns/{id}/leads/find-emails

Kicks 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.

bash
curl -X POST https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/leads/find-emails \
  -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
json
{ "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 }.

Was this page helpful?