Swimlane public API reference
An overview of every public API endpoint, with the fields you can send and read. For a hands-on start, see Create an API token and make your first request. The interactive version is at https://api.getswimlane.com/docs.
Basics
- Base URL:
https://api.getswimlane.com/v1 - Authentication:
Authorization: Bearer wpa_…, with a token created in Settings → API. - Scope: everything happens in the organization you last used in the app. Campaigns, reminders and recurring imports are scoped further, to the token's own user — they run on that person's LinkedIn session, so a token never sees a teammate's.
- Pagination (list endpoints):
page,per_page(default 50, maximum 100). Responses are{ "meta": {…}, "data": [ … ] }. - Limit: 100 requests per minute per user, with
X-RateLimit-*on every response. - Errors:
401bad token,403no organization,404resource.not_found,409the state forbids it,422validation.failedwith anerrorsarray,429rate_limit.exceeded. - Everything is logged in Settings → API: method, path, status and duration.
Imports — how contacts get in
POST /imports creates a list and fills it from LinkedIn. This is the entry point: every other endpoint works on contacts already in the workspace.
{
"source": "competitor_engagement",
"listName": "Engaged with Acme — September",
"profileUrl": "https://www.linkedin.com/in/acme-founder/",
"recurring": true
}
source |
Reads | Needs |
|---|---|---|
post_engagement |
who reacted to or commented on a post | postUrl |
competitor_engagement |
who engages with someone else's posts | profileUrl |
profile_engagement |
who engages with your own posts | — |
profile_visitors |
who viewed your profile in a date range | from, to |
people_search |
a people search written as a persona | persona |
topic_search |
a topic turned into a boolean search | topic, strategy |
suggested_contacts |
the people LinkedIn suggests to you | — |
- It answers
202, and the list is empty. The scraping runs in the background on your LinkedIn session; follow it withGET /queueor watch the liststatusgoscheduled → 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, and a run outside your virtual office hours is deferred rather than dropped.
- A source missing what it reads answers
422 import.missing_parameters. recurring: truealso registers a schedule. Manage them withGET /imports/recurring,PATCH /imports/recurring/:uuid({"isActive": false}pauses) andDELETE /imports/recurring/:uuid(the list and its contacts are kept). The interval is randomized within a range on every run, sofrequencyHoursreports the last one used rather than a cadence you can set — a metronome is an automation fingerprint.
Contacts
GET /contacts: newest first. Filters:search(first name, last name or email),list_uuid,tag_id,archived.GET /contacts/:uuid,POST /contacts,PATCH /contacts/:uuid,DELETE /contacts/:uuid.
Fields you can send (all optional): firstName, lastName, email, phone, website, headline, note, location, countryIso (2 letters), archived, linkedinId.
Fields you read: the above plus uuid, avatarUrl, followersCount, isPremium, isCreator, isInfluencer, listUuids, companyUuids, tagIds, pipelineStageUuid, createdAt, updatedAt.
Notes:
linkedinIdis the field that decides everything. It takes the public identifier or the profile URL it sits in (https://www.linkedin.com/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 infailed. It cannot be guessed later from a name or an email, so carry the profile URL when you push leads in from elsewhere.- It is unique per organization. A duplicate answers
409 contact.linkedin_id_takenand names the contact that already holds it, rather than merging two people. - Writing
noteorheadlinemarks the contact's cached ICP scores stale, so they are recomputed. pipelineStageUuidgives one stage, taken from the pipelines you can see.- The responses to
POSTandPATCHleavelistUuids,companyUuidsandtagIdsempty andpipelineStageUuidnull. Read the contact again withGETto get them. - Responses also carry a
scorefield. It is an internal value the app no longer shows. Don't build on it.
Lists
GET /lists,GET /lists/:uuid,POST /lists(namerequired,description,icon),PATCH,DELETE.POST /lists/:uuid/contacts: body{"contactUuids": ["…"]}. Contacts already in the list, and unknown uuids, are skipped without error.DELETE /lists/:uuid/contacts/:contactUuid.
Fields you read: uuid, name, description, icon, status, source, createdAt, updatedAt. source names the import that produced the list, or is null when someone created it by hand.
Adding to a list is not inert. If a running campaign uses it as its source and was created with continuous: true, the contacts you add are enrolled on its next scan — within a minute or so, they start receiving its invitations and messages.
Companies (read only)
GET /companies (filter: search), GET /companies/:uuid. Fields: uuid, name, linkedinId, logoUrl, createdAt, updatedAt.
Tags
GET /tags,POST /tags(namerequired,coloras#RRGGBB),PATCH /tags/:id,DELETE /tags/:id.GET /contacts/:uuid/tags,POST /contacts/:uuid/tagswith{"tagIds": [12, 18]}. It replaces all the contact's tags. Send[]to remove them all.
A tag is one row per name, shared by every workspace. "inbound" is the same tag everywhere. So renaming one that others also use copies it first and renames the copy, and deleting one only detaches it from your contacts. Neither can reach into someone else's workspace.
Pipelines and stages
GET /pipelines,POST /pipelines(namerequired,visibilityprivateorteam,icon),PATCH,DELETE.GET /pipelines/:uuid/stages,POST /pipelines/:uuid/stages,PATCH /pipelines/stages/:stageUuid,DELETE /pipelines/stages/:stageUuid.POST /pipelines/stages/:stageUuid/contactswith{"contactUuids": ["…"]}. A contact sits in one stage per pipeline, so this also moves it.DELETE /pipelines/stages/:stageUuid/contacts/:contactUuid.
Stage fields: uuid, name, icon, color, type, order, createdAt, updatedAt.
typeisdone,abandonedor null, and you can set it on create and update. Both values are terminal: Swimlane stops offering those contacts for follow-up. A "Won" column created without it keeps nudging you about deals you closed months ago.- A new pipeline comes with the same four stages the app creates — Cold, Connected, Engaged and Closed, the last already typed
done. - A teammate's private pipeline cannot be read or changed.
visibility: teamis the only way to share one.
Reminders
GET /reminders (filters: completed, contact_uuid, due_after, due_before, order), GET /reminders/:uuid, POST, PATCH, DELETE, and GET /contacts/:uuid/reminders.
A reminder is a personal to-do, scoped to the token's own user. The due date is stored at midnight — the app offers no hour selection, so a time you send is dropped rather than sorting against the app's.
Campaigns
GET /campaigns(filters:type,status),GET /campaigns/:uuid,POST,PATCH,DELETE.POST /campaigns/:uuid/start|pause|resume.GET|PUT /campaigns/:uuid/steps,GET /campaigns/:uuid/contacts,GET /campaigns/:uuid/stats.
Three calls take you from nothing to a running campaign: create a draft pointed at a list, PUT the flow, then start.
The flow is a tree sent in one array. 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. branchKey is "yes" or "no", nothing else: required under a condition, optional under connection_request (yes = the invitation was accepted) and send_message (yes = they answered), refused anywhere else. A flow that cannot work is refused with a 422 naming the problem.
Steps are frozen once a campaign leaves draft — enrolled contacts point at the step they are on. After start, enrollment runs in the background, so the stats stay empty for about a minute.
Deleting a campaign also cancels everything it had queued.
The queue and your account
GET /queue(filters:statepending/running/done,method,since),GET /queue/:id. Read-only: it reports what the app scheduled, is running or has finished,failedincluded.GET|PATCH /account,GET|PATCH /account/settings(daily automation limits, virtual office hours, tone of voice),GET /account/usage.
When nothing seems to happen, these two answer fastest. GET /account/usage puts every daily counter next to its ceiling — a counter at the limit is the usual reason a campaign went quiet. GET /queue shows what failed. 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.
What the API does not cover
There are no endpoints for conversations and messages: the API can tell you that someone replied, not what they wrote, and it cannot send a one-off message to a single person. Notes, ICP fit as a filter, and the activity feed are not exposed either.
To react to new or changed contacts without polling, use webhooks (see Set up webhooks). To hand the whole surface to an AI assistant instead of writing the calls, see Connect an AI assistant with the MCP server.
