Connecting Swimlane to your CRM with webhooks and the API
The webhook contract Swimlane sends, how to verify and retry it, and receiver patterns for HubSpot, Salesforce and Pipedrive via n8n, Make or your own code.
By Swimlane Updated September 14, 2026 9 min read
A webhook CRM integration is only as good as your knowledge of what the sender actually sends. Swimlane has no native HubSpot, Salesforce or Pipedrive sync. It gives you three signed contact webhooks, a public REST API, and works with n8n and Make. This guide documents the exact contract, then shows how to receive it in each CRM without duplicates.
What to sync, and what to leave in Swimlane
Decide this before writing any code. The LinkedIn conversation, its notes and its reminders stay in Swimlane, next to the inbox. Your CRM needs to know about a person, and later about a deal. Two mechanisms cover that:
- Webhooks (push) tell you a contact was created, edited or deleted, within about a minute.
- The API (pull) tells you everything else about that contact: headline, company, tags, pipeline stage.
Most robust integrations use both: the webhook as the trigger, the API as the source of detail.
The three webhook events
Swimlane sends exactly three events, plus a test.
contact.created: a contact is added to your workspace, whether imported, created from a conversation or added by hand.contact.updated: a contact’s own fields change, through an edit in the app or when an import or conversation sync brings new values for an existing contact. It does not fire on bulk edits, LinkedIn profile refreshes, tags, lists, pipeline moves, messages, replies or connection changes.contact.deleted: a contact is deleted individually. Bulk deletes send nothing.test: sent when you create a webhook or press “Test Webhook”.
There is no event for a reply, a tag, a stage change or an accepted invitation, and a webhook cannot be filtered by list or tag: it receives every matching event in the workspace.
Two delivery facts shape your receiver:
- Latency. Events are queued and a worker delivers them once a minute. Expect up to about a minute.
- One delivery per member webhook. An event goes to every member of the organisation that owns the contact, and each member’s active webhooks get their own copy. If three teammates each point a webhook at the same URL, you receive three deliveries. Give the CRM webhook one owner.
The payload
Every delivery is a POST with a JSON body and these headers:
Content-Type: application/jsonX-Webhook-Event: the event nameX-Webhook-Timestamp: an ISO 8601 dateX-Webhook-Signature: present only when the webhook has a secret
{
"event": "contact.updated",
"timestamp": "2026-09-14T09:12:03.412+00:00",
"data": {
"contactId": 4821,
"uuid": "3f0c5b1e-8d2a-4c1e-9f5b-2a7d6e1c9b40",
"firstName": "Jane",
"lastName": "Doe",
"email": null,
"linkedinId": "jane-doe-42",
"changes": ["headline", "email"]
}
}
uuidis the stable identifier, the one the public API uses. Key your CRM on it.contactIdis internal. Ignore it.linkedinIdis the public profile slug: buildhttps://www.linkedin.com/in/<linkedinId>.firstName,lastNameandemailcan benull. Most LinkedIn contacts have no email.changesexists only oncontact.updated. It lists the names of the fields that were submitted, not old and new values, and it can name fields whose value did not change.
Not in the payload: company, headline, location, ICP level, tags, lists, pipeline stage, messages.
Setting up a webhook in Swimlane
Go to Settings > Webhooks and press “Add Webhook”.
- Webhook URL. An HTTPS endpoint you control, or the webhook URL of an n8n or Make workflow.
- Events. Tick at least one.
- Maximum Retries. From 1 to 5, default 1. It is the total number of attempts, so 1 means no retry at all. Use 5 for a CRM.
- Generate secret key. Off by default: tick it. The secret is shown once, so store it right away. Editing the webhook offers “Regenerate secret key”.
Creating the webhook immediately sends a test event. The list then offers “Test Webhook” and an on/off toggle. Swimlane shows no delivery history, so log what you receive on your side.
Verifying the signature
The signature is an HMAC-SHA256 of the exact request body, keyed with your secret, sent as a lowercase hex digest with no sha256= prefix. The timestamp sits inside the signed body, so you can also reject stale deliveries. Each retry is re-signed with a fresh timestamp, so legitimate retries still pass.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
app.post('/swimlane', express.raw({type: 'application/json'}), async (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.SWIMLANE_WEBHOOK_SECRET)
.update(req.body)
.digest('hex')
const received = req.get('X-Webhook-Signature') ?? ''
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))
if (!valid) return res.status(401).end()
const event = JSON.parse(req.body.toString('utf8'))
if (Date.now() - Date.parse(event.timestamp) > 5 * 60 * 1000) return res.status(401).end()
if (event.event === 'test') return res.status(200).end()
await saveForProcessing(event) // a queue or a table: the CRM call happens later
res.status(202).end()
})
Verify on the raw bytes wherever your stack allows it. In n8n, where the Webhook node hands you parsed JSON, recompute the HMAC on JSON.stringify of the body: Swimlane signs compact JSON.stringify output, so it reproduces the same bytes. If a tool cannot check the signature at all, treat its webhook URL as a secret and read the contact back from the API before writing anything. A forged event then only makes you look up a contact that does not exist.
Swimlane publishes no list of sender IPs, so an IP allowlist is not an option. The signature is the control.
Retries, ordering and idempotency
- Success is any 2xx answer within 30 seconds. Anything else, or a timeout, is a failed attempt.
- Retries follow a 15 s, 30 s, 60 s, 120 s backoff, up to your “Maximum Retries”. Because the worker runs once a minute, real gaps round up to the minute. After the last attempt the delivery is marked failed and dropped.
- Duplicates happen: a retry after your endpoint timed out mid-processing, or several members’ webhooks on the same URL. The body differs between attempts, so you cannot deduplicate on a hash of it.
- Order is not guaranteed. Do not assume
createdarrives beforeupdated.
So make every write idempotent:
contact.created: search the CRM on the Swimlane uuid, create only if nothing is found.contact.updated: update if found, create if not. You may have missed the creation.contact.deleted: archive or flag the CRM record. Do not hard-delete sales history because someone cleaned a list.
Enriching events with the public API
Create a token in Settings > API. The same page shows the API host and links to the Swagger docs. Send it as Authorization: Bearer wpa_…; each token gets 100 requests per minute.
GET /v1/contacts/:uuidreturns the contact withheadline,location,linkedinId,companyUuids,listUuids,tagIdsandpipelineStageUuid.GET /v1/companies/:uuidgives the company name for your CRM’s company field.GET /v1/tagsmaps tag ids to names;GET /v1/pipelinesandGET /v1/pipelines/:uuid/stagesmap stage uuids to names.
Pulling instead: sync on a tag
Because no webhook fires on tags, stages or replies, “send this person to the CRM” is best expressed as a tag that a scheduled workflow reads:
- Tag the contact “to-crm”, by hand from the conversation or with a campaign’s “Add tag” step under a “Replied to message” condition.
- Every 15 minutes, n8n or Make calls
GET /v1/contacts?tag_id=<id>&per_page=100. - Upsert each contact in the CRM.
- Only after the CRM call succeeds, call
POST /v1/contacts/:uuid/tagswith the tags to keep. It replaces the whole set, so send the currenttagIdsminus “to-crm”, plus “in-crm” if you use one.
The API also works the other way: when the CRM marks a deal won, POST /v1/pipelines/stages/:stageUuid/contacts puts the contact in your Done stage in Swimlane, on any pipeline the token’s owner can see. The flow that hands replies to your CRM with n8n builds this end to end, and the n8n setup guide and the Make setup guide cover the connection itself.
HubSpot
HubSpot’s own “When a webhook is received” workflow trigger is limited to Data Hub Professional and Enterprise, and it only enrols records that already exist, matched on a property that requires unique values. It can update contacts you have synced before; it cannot create new ones. For contact.created, go through n8n or Make, which both have HubSpot modules, or your own endpoint calling HubSpot’s API.
- Create a custom property for the Swimlane uuid, with unique values required, and search on it before creating.
firstName→firstname,lastName→lastname.email→emailonly when it is notnull.- The LinkedIn URL goes into a custom property.
- Headline and company name come from the API call, not the payload.
Salesforce
Salesforce does not accept an unauthenticated POST into its objects, and Swimlane sends no OAuth token. Either use middleware (n8n and Make both connect to Salesforce), or expose an Apex REST class through a public Salesforce Site that verifies the signature before writing.
- Add a
Swimlane_UUID__ctext field marked External ID and unique on Lead or Contact, and upsert on it so duplicates cannot form. - Leads require
LastNameandCompany, and the payload has neither guaranteed. Fall back to thelinkedinIdfor the name and fetch the company from the API, with a placeholder if none is known.
Pipedrive
Pipedrive’s webhooks go out of Pipedrive; there is nothing inside it to receive Swimlane’s calls. Use n8n, Make or your own endpoint calling the Pipedrive API.
- Store the uuid in a text custom field on the person and search persons on it before creating.
- A person needs a name: join first and last name, or fall back to the
linkedinId. - Link the organisation from the company name returned by the API.
- Create deals from the tag or stage workflow above, not on
contact.created, or every imported commenter becomes a deal.
Testing checklist
- The
testevent gets a 2xx and never touches the CRM. - A wrong secret gets a 401.
- The same event delivered twice leaves one CRM record.
- A
contact.updatedfor an unknown uuid creates the record. - A
contact.deletedarchives rather than deletes. - With “Maximum Retries” at 5, an endpoint that is down for a minute still receives the event.
The team and API page summarises everything the workspace exposes to other tools.