Webhooks

Real-time event notifications for your integrations.

Last updated: May 9, 2026 · payload v2 · adds data.visit.id, data.reaction.id, and connection_status on profile visits

Overview

LeadShark sends webhooks via HTTP POST with JSON payloads. All webhooks include security headers for verification.

Manage webhooks programmatically. You can create, list, update, rotate the secret, delete, and test webhooks via the public REST API — no dashboard required. See Webhooks API for the full reference (Pro and above; new_profile_visit and new_like remain Apex-only).
Email Captured

email.captured

Pro
Lead Sent

lead.sent

Pro
Profile Visit

new.profile.visit

Apex
Post Like

new.like

Apex
Post Comment

new.comment

Pro

Authentication & Security

Security Headers

X-Webhook-SignatureHMAC-SHA256 signature
X-Webhook-EventEvent type
Content-Typeapplication/json

Signature Verification

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  return signature === expected;
}

Payload Schema (v2)

Back-compat

Every webhook body now ships a unified shape so a single handler can deal with new.profile.visit, new.like and new.comment without per-event branching. The unification is 100% additive — every legacy field you're consuming today is still emitted exactly as before.

Backwards-compat guarantee

  • Existing fields (commenter_id, reactor_id, post_id, post_url, visitor, profile_owner) keep firing on every payload — no consumer code change required.
  • v2 only adds fields (payload_version, data.lead.actor_id, data.post, data.reaction, and an explicit data.automation: null when there is no automation context).
  • Legacy fields will be supported for at least two full quarters after this release. We'll announce a removal date in Docs and via the changelog before retiring anything.

Common envelope

{
  "event": "new.like" | "new.comment" | "new.profile.visit" | "email.captured" | "lead.sent",
  "timestamp": "2026-05-07T12:38:50.258Z",
  "payload_version": 2,
  "data": {
    "lead": { /* always present, see below */ },
    "post": { "id": "urn:li:activity:...", "url": "https://www.linkedin.com/feed/update/..." },
    "automation": { "id": "uuid", "name": "..." } | null,
    /* event-specific blocks: comment, reaction, visitor, profile_owner */
    /* legacy fields (post_id, post_url, etc.) are still emitted alongside */
  }
}

payload_version: 2 applies to the LinkedIn-event payloads (new.comment, new.like, new.profile.visit). email.captured and lead.sent ship a slimmer payload — see their sections below for the exact shape.

Unified data.lead

FieldType
data.lead.actor_idstring
data.lead.namestring
data.lead.first_namestring
data.lead.titlestring
data.lead.linkedin_urlstring
data.lead.linkedin_usernamestring
data.lead.connection_statusstring
data.lead.commenter_idstring · legacy
data.lead.reactor_idstring · legacy
data.lead.reaction_typestring · legacy

Unified data.post · data.automation

FieldType
data.post.idstring
data.post.urlstring?
data.automationobject | null
data.post_idstring · legacy
data.post_urlstring? · legacy

Email Captured

Pro
Event: email.captured— When a lead provides their email in email-gated Pages.

Payload

{
  "event": "email.captured",
  "timestamp": "2026-05-09T11:43:42.659Z",
  "data": {
    "lead": {
      "name": "John Doe",
      "title": "Senior Marketing Manager",
      "email": "john.doe@example.com",
      "phone": "+15551234567",
      "linkedin_url": "https://linkedin.com/in/john-doe-12345",
      "linkedin_username": "john-doe-12345",
      "commenter_id": "ACoAAEXAMPLE444555666"
    },
    "automation": {
      "id": "58c132e6-648c-4079-8815-77a3b2ec1adc",
      "name": "Q4 Marketing Guide"
    },
    "post_id": "urn:li:activity:7372270383585984512",
    "link_index": 1,
    "meta": {
      "ip_address": "203.0.113.42",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
      "referrer": "https://www.linkedin.com/",
      "personalized_access": true
    }
  }
}

Fields

FieldType
eventstring
timestampstring
data.lead.namestring
data.lead.titlestring
data.lead.emailstring
data.lead.phonestring?
data.lead.linkedin_urlstring?
data.lead.linkedin_usernamestring?
data.lead.commenter_idstring
data.automationobject | null
data.post_idstring
data.link_indexnumber
data.meta.ip_addressstring
data.meta.user_agentstring
data.meta.referrerstring
data.meta.personalized_accessboolean

Lead Sent

Pro
Event: lead.sent— Fires when you push a lead from the dashboard via “Send to webhook”. Manual, user-triggered — not LinkedIn-driven.

Payload

{
  "event": "lead.sent",
  "timestamp": "2026-05-09T13:24:11.482Z",
  "data": {
    "lead": {
      "name": "Sarah Wilson",
      "first_name": "Sarah",
      "last_name": "Wilson",
      "title": "Content Marketing Specialist",
      "email": "sarah@example.com",
      "linkedin_url": "https://linkedin.com/in/sarah-wilson",
      "linkedin_username": "sarah-wilson",
      "commenter_id": "ACoAAEXAMPLE444555666",
      "connection_status": "Connected",
      "location": "San Francisco, CA",
      "icp_fit": true,
      "icp_score": 0.95
    }
  }
}

Fields

FieldType
eventstring
timestampstring
data.lead.namestring
data.lead.first_namestring?
data.lead.last_namestring?
data.lead.titlestring?
data.lead.emailstring?
data.lead.linkedin_urlstring?
data.lead.linkedin_usernamestring?
data.lead.commenter_idstring?
data.lead.connection_statusstring?
data.lead.locationstring?
data.lead.icp_fitboolean?
data.lead.icp_scorenumber?

lead.sent intentionally has no data.post / data.automation blocks — it is not tied to a LinkedIn surface event. Subscribe a webhook to lead_sent in Settings → Webhooks (or via the REST API) to receive it.

Profile Visit

Apex
Event: new.profile.visit— When someone visits your profile

Payload

{
  "event": "new.profile.visit",
  "timestamp": "2026-05-07T11:37:21.709Z",
  "payload_version": 2,
  "data": {
    "lead": {
      "name": "Jane Smith",
      "first_name": "Jane",
      "title": "Product Manager at Innovation Labs",
      "linkedin_url": "https://linkedin.com/in/jane-smith",
      "linkedin_username": "jane-smith",
      "connection_status": "2nd Connection",
      "actor_id": "ACoAAEXAMPLE987654321",
      "is_anonymous": false
    },
    "visit": { "id": "vi_a3f5b8c2e1d0a7f4" },
    "visit_timestamp": "2026-05-07T11:37:21.709Z",

    // ── Legacy fields (kept for back-compat) ──
    "visitor": {
      "id": "ACoAAEXAMPLE987654321",
      "name": "Jane Smith",
      "title": "Product Manager at Innovation Labs",
      "linkedin_url": "https://linkedin.com/in/jane-smith",
      "linkedin_username": "jane-smith",
      "connection_status": "2nd Connection",
      "is_anonymous": false
    },
    "profile_owner": {
      "id": "ACoAAEXAMPLE123456789",
      "name": "Your Name",
      "linkedin_url": "https://linkedin.com/in/your-name"
    },
    "meta": { "source": "linkedin", "timestamp": "2026-05-07T11:37:21.709Z" }
  }
}

Anonymized visitors (LinkedIn Premium)

When a LinkedIn Premium user views your profile in private mode, LinkedIn never reveals their identity. We pass that through honestly: linkedin_username and first_name are null, name is the descriptive ICP label LinkedIn returns (e.g. “Founder in the Tech industry from Hamburg”), linkedin_url is a search-results URL, and is_anonymous is true. Branch on data.lead.is_anonymous to skip handle/URL lookups for these rows.

{
  "event": "new.profile.visit",
  "timestamp": "2026-05-10T08:38:01.042Z",
  "payload_version": 2,
  "data": {
    "lead": {
      "name": "Founder in the Technology, Information and Internet industry from Greater Hamburg Area",
      "first_name": null,
      "title": "",
      "linkedin_url": "https://www.linkedin.com/search/results/people/?keywords=Founder&origin=WHO_VIEWED_ME&industry=6",
      "linkedin_username": null,
      "connection_status": "Not Connected",
      "actor_id": "visitor-1778402641042-0.17692646244689203",
      "is_anonymous": true
    },
    "visit": { "id": "vi_d4c8e9a2b1f7e3d0" },
    "visit_timestamp": "2026-05-10T08:38:01.042Z"
  }
}

data.visit.id is a deterministic synthetic id (sha256 of visitor_id::visit_timestamp, truncated, prefixed vi_). Stable across retries — use it as an idempotency token, the same way you'd use data.comment.id on new.comment.

data.lead.connection_status is now populated on profile visits — read from the connection-degree badge LinkedIn shows next to each visitor on the “Who Viewed My Profile” feed (same source the dashboard already surfaces on the Profile page). Visitors with no badge are reported as "Not Connected".

Post Like

Apex
Event: new.like— Auto-detected every ~2 hours from your 10 most recent posts. No per-post setup required.

Payload

{
  "event": "new.like",
  "timestamp": "2026-05-07T12:38:50.258Z",
  "payload_version": 2,
  "data": {
    "lead": {
      "name": "Michael Johnson",
      "first_name": "Michael",
      "title": "Sales Director",
      "linkedin_url": "https://linkedin.com/in/michael-johnson",
      "linkedin_username": "michael-johnson",
      "connection_status": "Connected",
      "actor_id": "ACoAAEXAMPLE111222333",

      // ── Legacy fields (kept for back-compat) ──
      "reactor_id": "ACoAAEXAMPLE111222333",
      "reaction_type": "like"
    },
    "post": {
      "id": "urn:li:activity:7372270383585984512",
      "url": "https://www.linkedin.com/feed/update/urn:li:activity:7372270383585984512"
    },
    "reaction": {
      "id": "re_b2e1d0a7f4a3f5b8",
      "type": "like"
    },
    "automation": null,

    // ── Legacy fields (kept for back-compat) ──
    "post_id": "urn:li:activity:7372270383585984512",
    "post_url": "https://www.linkedin.com/feed/update/urn:li:activity:7372270383585984512"
  }
}

data.reaction.id is a deterministic synthetic id (sha256 of post_id::reactor_id::reaction_type, truncated, prefixed re_). Stable across retries — use it as an idempotency token. Note that if a reactor changes their reaction (e.g. like praise), the id changes too, so you'll see a new payload rather than an update to the old one.

data.automation is always null on new.like — the worker that detects reactions runs independently of comment-automations, so there is no automation context to attach. The field is kept present (just null) so a consumer destructuring data.automation across like and comment events doesn't have to special-case it.

reaction.type reflects the actual LinkedIn reaction value — like, empathy, appreciation, interest, praise, entertainment, or maybe. The webhook fires for any of them.

Post Comment

Pro
Event: new.comment— When someone comments on your post

Payload

{
  "event": "new.comment",
  "timestamp": "2026-05-07T12:42:27.124Z",
  "payload_version": 2,
  "data": {
    "lead": {
      "name": "Sarah Wilson",
      "first_name": "Sarah",
      "title": "Content Marketing Specialist",
      "linkedin_url": "https://linkedin.com/in/sarah-wilson",
      "linkedin_username": "sarah-wilson",
      "connection_status": "2nd Connection",
      "actor_id": "ACoAAEXAMPLE444555666",

      // ── Legacy field (kept for back-compat) ──
      "commenter_id": "ACoAAEXAMPLE444555666"
    },
    "comment": {
      "id": "7371940875070795776",
      "text": "Great insights! Thanks for sharing.",
      "created_at": "2026-05-07T16:21:08.656Z"
    },
    "post": {
      "id": "urn:li:activity:7372270383585984512",
      "url": "https://www.linkedin.com/feed/update/urn:li:activity:7372270383585984512"
    },
    "automation": { "id": "uuid", "name": "Content Engagement" },

    // ── Legacy fields (kept for back-compat) ──
    "post_id": "urn:li:activity:7372270383585984512",
    "post_url": "https://www.linkedin.com/feed/update/urn:li:activity:7372270383585984512"
  }
}

Setup Guide

  1. Configure webhook endpoint in Dashboard Settings or via the REST API
  2. Select event types to receive
  3. Save your webhook secret for verification (returned only at creation / rotation)
  4. Ensure endpoint returns 2xx status
  5. Optional: send a test event from the dashboard or POST /api/v1/webhooks/:id/test

Important

  • • Webhooks timeout after 5-10 seconds
  • • Return 2xx status to acknowledge

Code Examples

Node.js / Express

const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());

app.post('/webhook', (req, res) => {
  const sig = req.headers['x-webhook-signature'];
  const expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(JSON.stringify(req.body)).digest('hex');

  if (sig !== expected) return res.status(401).json({ error: 'Invalid' });

  // v2 unified shape — same field names across like / comment / visit.
  // Falls back to legacy field names if a v1 payload arrives.
  const { event, data } = req.body;
  const actorId =
    data.lead?.actor_id ??
    data.lead?.commenter_id ?? // legacy: new.comment
    data.lead?.reactor_id ??   // legacy: new.like
    data.visitor?.id;          // legacy: new.profile.visit
  const postId = data.post?.id ?? data.post_id;

  switch (event) {
    case 'email.captured':
      console.log('Email captured:', data.lead.email);
      break;
    case 'new.comment':
      console.log('Comment from', actorId, 'on post', postId, ':', data.comment.text);
      break;
    case 'new.like':
      console.log(actorId, 'reacted', data.reaction?.type ?? data.lead.reaction_type, 'on', postId);
      break;
    case 'new.profile.visit':
      console.log('Profile visited by', actorId);
      break;
  }
  res.json({ received: true });
});

Python / Flask

from flask import Flask, request, jsonify
import hmac, hashlib, json, os

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    sig = request.headers.get('X-Webhook-Signature')
    secret = os.environ.get('WEBHOOK_SECRET')
    expected = hmac.new(secret.encode(),
        json.dumps(request.json, separators=(',',':')).encode(),
        hashlib.sha256).hexdigest()
    
    if not hmac.compare_digest(sig, expected):
        return jsonify({'error': 'Invalid'}), 401
    
    return jsonify({'received': True})

Need Help?

Contact our team for webhook support.

Contact