Leads, lists and imports

How Swarmhit models leads, how lists group them for targeting and exclusion, and how background imports pull leads from a LinkedIn search.

Every person you reach out to is a lead: a single entry in your workspace address book. Lists are named buckets of leads used for campaign targeting and exclusion. Lead imports are background jobs that fill lists from a LinkedIn or Sales Navigator search. This guide covers the lifecycle of all three; the full endpoint details live in the Leads, Lists and Lead Imports reference sections.

Leads: the workspace address book

A lead is identified by its LinkedIn profile. profileUrl and publicIdentifier (the profile URL slug) are the only fields guaranteed to be set at creation. The same person is one lead across every list and campaign: the lead document carries listIds and campaignIds so you can see everywhere it is used.

In a workspace with the email channel, a lead can also be added by its email address alone. Such a lead has no LinkedIn profile, so its profileUrl and publicIdentifier are null, and it is matched on its address instead (case-insensitive). In a campaign it stops at the first LinkedIn step that would need an answer (an invite, a message, a connection check) unless the sequence routes it with if_has_linkedin (see Step types). If you later add the same person with a LinkedIn profileUrl and the same email, or set profileUrl on the lead with PATCH /leads/{id}, that lead gains the LinkedIn profile instead of a second lead being created. A lead without a LinkedIn profile must keep a valid email: clearing it is rejected with 400.

Leads are enriched lazily. Identity fields (firstName, lastName, headline, picture) are resolved from LinkedIn the first time the lead is actually contacted, not when it is created, so no profile lookup is spent on a lead that is never reached. Until then those fields are null and enrichedAt is null. If you need names immediately (for example to render your own UI), supply them in the request body when you add the lead: caller-supplied fields always win over the best-effort LinkedIn lookup.

Other fields worth knowing:

FieldMeaning
tags / autoTagsManual tags you control vs. tags applied automatically by campaigns and the inbox.
customVariablesFree-form templating values keyed by name, surfaced in messages as {{key}}. Shared across every campaign the lead is in.
openProfileWhether the profile accepts credit-free InMails (from Sales Navigator / Recruiter senders, within a monthly allowance). null until the lead is first contacted.
enrichedAtWhen the lead's identity was last filled in from LinkedIn.
sourceWhere the lead came from.

Creating leads (upsert, not POST)

There is no standalone create-lead endpoint. Leads are created where you use them, always with upsert semantics: pass an array of { profileUrl, ... } objects and Swarmhit matches existing leads on their LinkedIn identifier (or, for an item added by email alone, on its address), updates them with any new fields, and creates the rest. The two entry points are:

  • Add leads to a list: upserts the leads and attaches them to the list in one call.
  • Add leads to a campaign: upserts the leads and enrolls them; leads already in the campaign are skipped. They are also saved to the campaign's own list (created on first use and named "<campaign name>'s leads", listId on the campaign). That list mirrors the campaign: adding leads to it enrolls them, removing leads from it removes them from the campaign. See the campaigns guide.

Both accept up to 1000 leads per request and return { inserted, skipped } counts. The item shape (LeadInput) takes profileUrl (required, unless the workspace has the email channel and the item has a valid email) plus optional firstName, lastName, headline, jobTitle, company, email, location and customVariables. Leads also carry a read-only emailStatus (bounced or complained) once a campaign email to their address came back that way, and a read-only emailOptOut, true once they unsubscribed or reported an email as spam; email steps skip such leads.

Leads also arrive via lead imports and the prospecting database.

Updating and deleting leads

PATCH /leads/{id} accepts any subset of fields. Two semantics to note:

  • tags replaces the lead's manual tags (autoTags are never touched).
  • customVariables is merged per key; pass an empty string to clear a key.
curl -X PATCH https://app.swarmhit.com/api/v1/leads/LEAD_ID \
  -H "Authorization: Bearer swh_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "jobTitle": "VP Engineering", "tags": ["warm", "q3"] }'

A lead's campaign progress is a separate object: only interest is editable there, via Set lead interest. Everything else about the person lives on the lead itself.

DELETE /leads/{id} removes the lead from the address book.

Finding and inspecting leads

GET /leads supports limit / offset paging and a search parameter that matches case-insensitively on name, headline, job title, email, company or the profile slug. A full LinkedIn profile URL is reduced to its slug first, so a lead that has never been contacted (and therefore has no name yet) is still findable by its URL:

curl "https://app.swarmhit.com/api/v1/leads?search=linkedin.com/in/jane-doe" \
  -H "Authorization: Bearer swh_live_..."

GET /leads/{id}/activity returns a merged, newest-first timeline of campaign step transitions and inbox messages for the lead, capped at 200 events. Useful for building a "what happened with this person" view without stitching campaign and inbox data yourself.

Lists

A list is a named bucket of leads: id, name, color, leadCount, timestamps. Lists serve three roles:

  1. Campaign targeting. Pass { "listId": "..." } to Add leads to a campaign to enroll every lead in the list.
  2. Exclusion. Set excludeListIds on a campaign (at create or update): members of those lists are never contacted by that campaign. Typical use: a "customers" or "do not contact" list shared across campaigns. On update, excludeListIds replaces the campaign's exclusion lists; pass an empty array to clear them.
  3. Import destination. Every lead import lands its leads in a list.

Create a list:

curl -X POST https://app.swarmhit.com/api/v1/lists \
  -H "Authorization: Bearer swh_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "Conference leads" }'

Add (and thereby create) leads in it:

curl -X POST https://app.swarmhit.com/api/v1/lists/LIST_ID/leads \
  -H "Authorization: Bearer swh_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "leads": [
      { "profileUrl": "https://www.linkedin.com/in/jane-doe", "firstName": "Jane", "lastName": "Doe", "company": "Acme" },
      { "profileUrl": "https://www.linkedin.com/in/john-roe", "email": "john@roe.dev" }
    ]
  }'

Membership is loose by design: removing a lead from a list or deleting the list never deletes the leads themselves, only the membership.

A lead import is a background job that runs a LinkedIn or Sales Navigator people search through one of your connected sender accounts and stores the results as leads. POST /lead-imports returns immediately; a background worker picks the job up within a minute and fetches the search page by page until maxLeads is reached or the search runs out of results.

Provide either:

  • url: a search URL copied from LinkedIn (Classic or Sales Navigator), or
  • params: a structured filter blob with api (classic | sales_navigator) and category (people).

In params, filter ids (location, industry, company, ...) must be strings ("2579", not 2579). Classic search uses flat arrays (company: ["2579"]) while Sales Navigator uses include/exclude objects (company: { include: ["2579"] }).

Destination and limits: pass listId to import into an existing list, or omit it and a new list is auto-created (name it with listName) and returned in the response under destination. To import straight into a campaign, pass campaignId instead: the leads land in the campaign's own list (created if it doesn't exist yet) and are enrolled in the campaign page by page, subject to its exclusion rules. maxLeads caps how many leads are stored (clamped to 1 to 2500, default 100), and tags are applied to leads as they are imported.

curl -X POST https://app.swarmhit.com/api/v1/lead-imports \
  -H "Authorization: Bearer swh_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "ACCOUNT_ID",
    "url": "https://www.linkedin.com/search/results/people/?keywords=cto%20fintech",
    "listName": "Fintech CTOs",
    "maxLeads": 500,
    "tags": ["fintech"]
  }'

Job lifecycle and polling

An import moves through these statuses:

StatusMeaning
pendingQueued; the worker picks it up within a minute.
runningFetching search pages and storing leads.
pausedOn hold; restart it with resume.
completedFinished: maxLeads reached or the search exhausted.
failedStopped on an error (see the error field); can be resumed.
cancelledStopped by cancel.

Poll GET /lead-imports/{id} to track progress. The progress object reports fetchedPages, fetchedItems, importedLeads, skippedItems and (when the search reports one) totalCount; nextPageAt tells you when the worker will next tick the job. GET /lead-imports lists jobs and filters by status.

Control endpoints: cancel a running import, resume a paused or failed one, and delete an import only once it is terminal (completed, failed or cancelled).

Instead of polling per lead, you can subscribe a webhook to the lead.imported event to be notified as leads land.

Reference

On this page