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. On PATCH the value must be a LinkedIn profile URL (https://www.linkedin.com/in/...), and it is rejected with 400 if another lead already has that profile. 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 (unless you supplied them) 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. At first contact, the values LinkedIn returns for those four fields replace the ones you supplied; your other fields (jobTitle, company, email, location, customVariables) are kept.

Other fields worth knowing:

FieldMeaning
tags / autoTagstags are the manual tags you control. autoTags holds automatically applied tags and is currently always empty.
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 (more is rejected with 400). 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. Pass the public profile URL with its slug in lower case, such as https://www.linkedin.com/in/jane-doe. A slug with capital letters or underscores, which includes member-id URLs (/in/ACoAA...), is not recognised as a LinkedIn profile: the item is skipped, or, with the email channel and a valid email, added by its address alone.

The two responses differ:

  • Add to a list returns { inserted, skipped, enrolled }. inserted counts the items saved to the list, whether the lead is new or already existed; skipped counts the items that identify no one; enrolled is how many were enrolled when the list is a campaign's own list (otherwise 0).
  • Add to a campaign returns { inserted, excluded, listId }. inserted counts new enrollments, excluded the leads the campaign's exclusion rules kept out, and listId is the campaign's own list. Leads already in the campaign and items that identify no one are not counted. Enrolling a whole list with listId returns { inserted, skipped, excluded, listId, listName } instead, where skipped counts the list's leads already in the campaign.

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 firstName, lastName, headline, jobTitle, company, email, location, notes, profileUrl, tags and customVariables. null or an empty string clears a field, except profileUrl, which cannot be cleared. Two semantics to note:

  • tags replaces the lead's manual tags (trimmed, deduplicated, at most 50; 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 and from every list. Its campaign memberships are kept for the record but lose their leadId, and a membership still in progress is marked failed (error Lead missing) the next time it runs instead of contacting the person. To take a lead out of a single campaign, remove it from that campaign instead.

Finding and inspecting leads

GET /leads returns the newest leads first, with limit / offset paging and a search parameter that matches case-insensitively on first name, last name, headline, job title, email, company or the profile slug. The whole search string must appear within one of those fields, so search one name at a time (jane, not jane doe). 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 the lead's campaign timeline across every campaign it is in, newest first and capped at 200 events: step results and events such as invite_sent, invite_accepted, reply_received, paused or interest_changed. Each event has kind (always step), at, campaignId, campaignLeadId, stepType, ok, branch, error and result. Inbox messages are not part of it; read a conversation through the inbox.

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. Deleting a list also takes it out of every campaign's excludeListIds, so its members stop being excluded. Deleting a campaign's own list leaves the campaign's leads enrolled; the campaign starts a new own list the next time leads are added to it.

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), category (people, the default) and at least one filter.

Sending both, or neither, is rejected with 400. accountId must be a connected LinkedIn sender in the workspace, and a Sales Navigator search needs a sender on Sales Navigator; otherwise, or when the listId or campaignId you pass does not exist, the request is rejected with 400.

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 (it takes precedence over listId): 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 (default 100, at most 2500; leads already in the workspace count too). tags (up to 20, 50 characters each) are added to the leads the import creates; leads that were already in the workspace keep their tags unchanged.

Each stored lead also counts toward the sender's daily search-import limit. When it is reached, the import waits until midnight UTC: it stays running, and nextPageAt shows when it continues.

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
pendingListed in the enum but not used today: a new import starts as running.
runningFetching search pages and storing leads; the worker fetches the first page within a minute of creation.
pausedOn hold, for example after LinkedIn rate-limited the search (see the error field); 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 or paused import, resume a paused or failed one, and delete an import only once it is terminal (completed, failed or cancelled; otherwise 400). Cancel and resume return the import unchanged when it is not in a state they apply to.

Instead of polling per lead, you can subscribe a webhook to the lead.imported event to be notified as leads land. It fires for each lead the import adds to the workspace; leads that were already there do not trigger it.

Reference

On this page