Skip to main content
All articles

Automation

Your LinkedIn CRM has an API — here is what it can actually do

Swimlane's public API covers the whole loop: import contacts from LinkedIn, work them in the CRM, build and run campaigns. A tour with the calls that matter and the traps worth knowing.

By Swimlane 6 min read

Most “CRM APIs” let you read rows. This one runs the product: it imports people off LinkedIn, files them, and sends the sequences. Anything you can do in Swimlane, a script can do — on the same LinkedIn session, under the same daily limits.

Below is the shape of it, endpoint by endpoint, with the parts that surprise people first.

Create a token in Settings → API. It is shown once, and it is sent as Authorization: Bearer wpa_… on every call. The full reference is at api.getswimlane.com/docs.

Contacts get in through an import

This is the part people look for last and need first. POST /v1/imports creates a list and fills it from LinkedIn:

curl -X POST https://api.getswimlane.com/v1/imports \
  -H "Authorization: Bearer wpa_…" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "competitor_engagement",
    "listName": "Engaged with Acme — September",
    "profileUrl": "https://www.linkedin.com/in/acme-founder/",
    "recurring": true
  }'

Seven sources are available: the people who engaged with a post (post_engagement), with a competitor’s posts (competitor_engagement) or with your own (profile_engagement); who viewed your profile in a date range (profile_visitors); a people search written as a persona (people_search); a topic turned into a boolean search (topic_search); and the people LinkedIn suggests to you (suggested_contacts).

Three things to expect:

  • It answers 202, and the list is empty. The scraping runs in the background on your own LinkedIn session. Watch GET /v1/queue, or the list’s status going scheduled → importing → done.
  • The daily import limit is claimed when the task runs, not when you call. An import started past it waits for tomorrow instead of failing, so queueing a day’s sourcing up front is safe. Same for your virtual office hours: a run outside them is deferred, not dropped.
  • recurring: true keeps the list filling every few hours. The interval is randomized within a range on every run — a metronome is an automation fingerprint, so you cannot pin it. Manage the schedules at GET|PATCH|DELETE /v1/imports/recurring.

Contacts, and the field that decides everything

GET /v1/contacts searches names and emails, filters by list, tag or archived state. POST, PATCH and DELETE do what you expect.

The field to care about is linkedinId:

{ "firstName": "Sofia", "lastName": "Marchetti", "linkedinId": "https://www.linkedin.com/in/sofia-marchetti/" }

It takes the public identifier or the profile URL it sits in, and it is what makes a contact reachable. A contact without one is a CRM record and nothing else — every campaign step that touches LinkedIn refuses it, retries three times and lands in failed. It cannot be guessed later from a name or an email either. If you are pushing leads in from elsewhere to sequence them, carry the profile URL.

It is unique per workspace: a duplicate answers 409 and names the contact that already holds it, rather than quietly merging two people.

Lists, tags and pipelines

POST /v1/lists/<uuid>/contacts adds contacts in bulk; ones already in the list are skipped, so it is safe to repeat. One thing to know before you automate it: if a running continuous campaign uses that list as its source, the contacts you add start receiving its invitations within the minute. That is the point of the pattern — a sourcing import plus a continuous campaign is a pipeline that feeds itself — but it means the endpoint is not inert.

Tags are shared: the tags table holds one row per name, and “inbound” is the same tag in every workspace. So renaming a tag that others use copies it first and renames the copy, and deleting one only detaches it from your contacts. POST /v1/contacts/<uuid>/tags replaces the whole set — send every id the contact should end up with, or the ones you leave out are removed.

Pipelines carry one more field worth setting: a stage’s type, done or abandoned. Both are terminal, and Swimlane stops offering those contacts for follow-up. A “Won” column created without it keeps nudging you about deals you closed months ago.

Campaigns, end to end

Three calls, in order.

# 1. a draft, pointed at the list the import filled
POST /v1/campaigns
{"name": "Acme engagers", "type": "outbound", "listUuid": "…", "continuous": true}

# 2. the flow, as a tree sent in one array
PUT /v1/campaigns/<uuid>/steps

# 3. it starts sending
POST /v1/campaigns/<uuid>/start

The flow is where the care goes. Each step carries an id you choose and points at its parent with parentId; a parent must appear earlier in the array, and exactly one step has no parent. Branching is branchKey, and it is "yes" or "no" — nothing else. Under a condition it is required, and its meaning follows the parent: for a connection_request, yes is “the invitation was accepted”; for a send_message, yes is “they answered”.

{"steps": [
  {"id": "invite", "type": "connection_request"},
  {"id": "connected?", "type": "condition", "parentId": "invite",
   "conditionType": "is_connected", "delayDays": 3},
  {"id": "intro", "type": "send_message", "parentId": "connected?", "branchKey": "yes",
   "messageTemplate": "Thanks for connecting! What are you working on this quarter?"},
  {"id": "retry-later", "type": "add_tag", "parentId": "connected?", "branchKey": "no",
   "tagName": "invitation-pending"}
]}

A flow that cannot work is refused with a 422 naming the problem — a condition child with no branch, a branch under a step that does not branch, two children on the same branch, a step missing what its type runs on. The full guide, with the branching example above in one piece, is in the PUT /steps description in the reference.

Steps are frozen once a campaign leaves draft: enrolled contacts point at the step they are on, so a replacement would strand them. Name, description and targeting stay editable at any status.

After start, enrollment runs in the background — GET /v1/campaigns/<uuid>/stats stays empty for about a minute. That is not a failure.

When nothing seems to happen

Two endpoints answer that question faster than anything else.

GET /v1/account/usage puts every daily counter next to the limit set for it. A counter at its ceiling is the usual reason a campaign went quiet: the quotas exist so an account looks like a person using LinkedIn, and they apply to API-triggered work exactly as they do to the app.

GET /v1/queue shows what is scheduled, running or finished, failed included. Between the two, “why has nothing been sent since Tuesday” is normally a thirty-second question.

The rules of the road

  • 100 requests a minute per token, with X-RateLimit-* on every response.
  • A token acts as its owner, and campaigns run on that person’s LinkedIn session — so a token never sees or touches a teammate’s campaigns.
  • Every call is logged in Settings → API, with method, path, status and duration. Useful when a workflow misbehaves at 3 a.m.
  • Errors are one shape: {"error": "domain.action.reason", "message": "…"}. 409 means the state forbids it, 422 means the payload is incoherent.

If you would rather describe what you want than write the calls, the same surface is available to AI assistants: see connecting Claude to your LinkedIn CRM.

Do this with Swimlane

Everything in this article, minus the manual part.

Start my free trial

See how it works in Swimlane AI assistant

Keep reading