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.

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, updates them with any new fields, and creates the rest. The two entry points are:

Both accept up to 1000 leads per request and return { inserted, skipped } counts. The item shape (LeadInput) takes profileUrl (required) plus optional firstName, lastName, headline, jobTitle, company, email, location and customVariables.

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