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. WatchGET /v1/queue, or the list’sstatusgoingscheduled → 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: truekeeps 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 atGET|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": "…"}.409means the state forbids it,422means 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.