Webhooks
Real-time event notifications for your integrations.
Last updated: May 7, 2026 · payload v2
Overview
LeadShark sends webhooks via HTTP POST with JSON payloads. All webhooks include security headers for verification.
new_profile_visit and new_like remain Apex-only).email.captured
new.profile.visit
new.like
new.comment
Authentication & Security
Security Headers
X-Webhook-Signature— HMAC-SHA256 signatureX-Webhook-Event— Event typeContent-Type— application/jsonSignature 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-compatEvery 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 explicitdata.automation: nullwhen 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",
"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 */
}
}Unified data.lead
| Field | Type |
|---|---|
| data.lead.actor_id | string |
| data.lead.name | string |
| data.lead.first_name | string |
| data.lead.title | string |
| data.lead.linkedin_url | string |
| data.lead.linkedin_username | string |
| data.lead.connection_status | string |
| data.lead.commenter_id | string · legacy |
| data.lead.reactor_id | string · legacy |
| data.lead.reaction_type | string · legacy |
Unified data.post · data.automation
| Field | Type |
|---|---|
| data.post.id | string |
| data.post.url | string? |
| data.automation | object | null |
| data.post_id | string · legacy |
| data.post_url | string? · legacy |
Email Captured
Proemail.captured— When a lead provides their emailPayload
{
"event": "email.captured",
"timestamp": "2025-09-15T11:43:42.659+00:00",
"data": {
"lead": {
"name": "John Doe",
"title": "Senior Marketing Manager",
"email": "john.doe@example.com",
"linkedin_username": "john-doe-12345"
},
"automation": {
"id": "58c132e6-648c-4079-8815-77a3b2ec1adc",
"name": "Q4 Marketing Guide"
}
}
}Fields
| Field | Type |
|---|---|
| event | string |
| timestamp | string |
| data.lead.email | string |
| data.automation.id | string |
Profile Visit
Apexnew.profile.visit— When someone visits your profilePayload
{
"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",
"actor_id": "ACoAAEXAMPLE987654321"
},
"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"
},
"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" }
}
}Profile visits do not carry network-distance data, so data.lead.connection_status is omitted on this event.
Post Like
Apexnew.like— Requires "Require like" enabled on automationPayload
{
"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": { "type": "like" },
"automation": { "id": "uuid", "name": "Engagement Campaign" },
// ── Legacy fields (kept for back-compat) ──
"post_id": "urn:li:activity:7372270383585984512",
"post_url": "https://www.linkedin.com/feed/update/urn:li:activity:7372270383585984512"
}
}Likes captured outside an automation context (i.e. on posts where you have not enabled "Require like" but have enrolled in like tracking) will arrive with data.automation: null.
Post Comment
Pronew.comment— When someone comments on your postPayload
{
"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
- Configure webhook endpoint in Dashboard Settings or via the REST API
- Select event types to receive
- Save your webhook secret for verification (returned only at creation / rotation)
- Ensure endpoint returns 2xx status
- 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.
