Webhook Events
Subscribe to events and receive real-time HTTP POST callbacks to your endpoint. Each event includes a signed JSON payload with full context.
HTTP Headers
Every webhook POST request includes these headers:
| Header | Description |
|---|---|
| Content-Type | Always application/json |
| X-Webhook-Signature | HMAC-SHA256 signature: t=timestamp,v1=hash |
| X-Webhook-Event | The event type, e.g. lead.replied |
| User-Agent | Always WeezlyOutreach/1.0 |
Response Requirements
Your endpoint must respond with a 2xx status code within 10 seconds. Non-2xx responses or timeouts are treated as failures. After 10 consecutive failures, the webhook is automatically disabled. You can re-enable it from Settings → API.
Verifying Signatures
Every webhook delivery includes an X-Webhook-Signature header. Verify this signature to ensure the payload was sent by Weezly Outreach and hasn't been tampered with.
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const parts = {};
signature.split(',').forEach(part => {
const [key, value] = part.split('=');
parts[key] = value;
});
const timestamp = parts['t'];
const expectedSig = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
return parts['v1'] === expectedSig;
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhookSignature(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = req.body;
console.log(`Received event: ${event}`, data);
res.status(200).json({ received: true });
});import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_signature(payload: str, signature: str, secret: str) -> bool:
parts = dict(p.split('=', 1) for p in signature.split(','))
timestamp = parts.get('t', '')
expected = hmac.new(
secret.encode(),
f"{timestamp}.{payload}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(parts.get('v1', ''), expected)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature', '')
payload = request.get_data(as_text=True)
if not verify_signature(payload, signature, WEBHOOK_SECRET):
return jsonify(error='Invalid signature'), 401
data = request.get_json()
print(f"Event: {data['event']}", data['data'])
return jsonify(received=True), 200Events on this page
Payload Structure
Every webhook payload follows the same top-level structure. The data field contains event-specific fields documented below.
{
"event": "event.type",
"data": {
"...event-specific fields": "..."
},
"timestamp": "2026-06-05T14:32:01.234Z",
"webhookId": "cm3wh789pqr"
}Campaign Step Types
The step.type field in campaign.step.completed and campaign.step.failed events will be one of:
| Type | Description |
|---|---|
| CONNECTION_REQUEST | Connection Request |
| MESSAGE | Direct Message |
| VIDEO_MESSAGE | Video Message |
| VOICE_NOTE | Voice Note |
| INMAIL | InMail |
| VIEW_PROFILE | Profile View |
| LIKE_POST | Like Post |
| REACT_TO_POST | React to Post |
| COMMENT_ON_POST | Comment on Post |
| FOLLOW_PROFILE | Follow Profile |
Event Reference
lead.repliedCampaignsFired when a lead sends a reply message to your outreach. This event triggers immediately when the reply is detected.
{
"event": "lead.replied",
"data": {
"campaignId": "cm3abc123def456",
"campaignName": "Q1 SaaS Founders Outreach",
"executionId": "cm3exec789ghi",
"leadId": "cm3lead456jkl",
"leadName": "Sarah Chen",
"lead": {
"id": "cm3lead456jkl",
"firstName": "Sarah",
"lastName": "Chen",
"fullName": "Sarah Chen",
"headline": "CEO at TechStartup",
"jobTitle": "CEO",
"company": "TechStartup Inc.",
"email": "sarah@techstartup.com",
"phone": null,
"location": "San Francisco, CA",
"linkedinUrl": "https://linkedin.com/in/sarahchen",
"linkedinPublicId": "sarahchen",
"photoUrl": "https://media.licdn.com/.../sarah.jpg",
"connectionStatus": "CONNECTED",
"source": "SEARCH"
},
"accountId": "cm3acc123mno",
"accountName": "John Smith",
"message": {
"text": "Hi John, thanks for reaching out! I'd love to chat about this.",
"chatId": "chat_abc123def456",
"receivedAt": "2026-06-05T14:32:00.000Z"
}
},
"timestamp": "2026-06-05T14:32:01.234Z",
"webhookId": "cm3wh789pqr"
}// When a lead replies to your outreach
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'lead.replied') {
const { lead, message, campaignName } = data;
console.log(`${lead.fullName} replied to "${campaignName}"`);
console.log(`Message: ${message.text}`);
// Example: Send Slack notification
await slack.chat.postMessage({
channel: '#sales-replies',
text: `🔔 ${lead.fullName} (${lead.company}) replied!\n> ${message.text}`,
});
// Example: Update your CRM
await crm.contacts.update(lead.email, {
status: 'replied',
lastActivity: message.receivedAt,
});
}
res.status(200).json({ received: true });
});connection.acceptedCampaignsFired when a lead accepts your LinkedIn connection request. Includes the lead's profile data and the campaign context.
{
"event": "connection.accepted",
"data": {
"campaignId": "cm3abc123def456",
"campaignName": "Q1 SaaS Founders Outreach",
"executionId": "cm3exec789ghi",
"leadId": "cm3lead456jkl",
"leadName": "Marcus Johnson",
"lead": {
"id": "cm3lead456jkl",
"firstName": "Marcus",
"lastName": "Johnson",
"fullName": "Marcus Johnson",
"headline": "VP Engineering at ScaleUp",
"jobTitle": "VP Engineering",
"company": "ScaleUp Solutions",
"email": "marcus@scaleup.com",
"phone": null,
"location": "Austin, TX",
"linkedinUrl": "https://linkedin.com/in/marcusjohnson",
"linkedinPublicId": "marcusjohnson",
"photoUrl": "https://media.licdn.com/.../marcus.jpg",
"connectionStatus": "CONNECTED",
"source": "SEARCH"
},
"accountId": "cm3acc123mno",
"accountName": "John Smith",
"acceptedAt": "2026-06-05T09:15:00.000Z"
},
"timestamp": "2026-06-05T09:15:02.456Z",
"webhookId": "cm3wh789pqr"
}// When a lead accepts your connection request
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'connection.accepted') {
const { lead, campaignName, accountName } = data;
console.log(`${lead.fullName} accepted connection from ${accountName}`);
// Example: Add to CRM pipeline
await crm.deals.create({
contact: lead.fullName,
company: lead.company,
source: `LinkedIn - ${campaignName}`,
stage: 'connected',
});
}
res.status(200).json({ received: true });
});campaign.completedCampaignsFired when all leads in a campaign have reached a terminal state (completed, replied, or failed). Includes final campaign statistics.
{
"event": "campaign.completed",
"data": {
"campaignId": "cm3abc123def456",
"campaignName": "Q1 SaaS Founders Outreach",
"stats": {
"totalLeads": 150,
"completed": 120,
"replied": 18,
"failed": 12,
"connectionsSent": 150,
"connectionsAccepted": 45,
"messagesSent": 45,
"connectionRate": 30,
"replyRate": 12
},
"startedAt": "2026-05-20T08:00:00.000Z",
"completedAt": "2026-06-05T16:00:00.000Z"
},
"timestamp": "2026-06-05T16:00:05.789Z",
"webhookId": "cm3wh789pqr"
}// When a campaign finishes all leads
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'campaign.completed') {
const { campaignName, stats } = data;
console.log(`Campaign "${campaignName}" completed!`);
console.log(`Results: ${stats.replied} replies from ${stats.totalLeads} leads`);
console.log(`Connection rate: ${stats.connectionRate}%`);
console.log(`Reply rate: ${stats.replyRate}%`);
// Example: Send summary email
await email.send({
to: 'team@company.com',
subject: `Campaign "${campaignName}" — ${stats.replyRate}% reply rate`,
body: `Completed with ${stats.replied} replies from ${stats.totalLeads} leads.`,
});
}
res.status(200).json({ received: true });
});campaign.launchedCampaignsFired when a campaign is launched and begins execution. Includes the campaign configuration and total lead count.
{
"event": "campaign.launched",
"data": {
"campaignId": "cm3abc123def456",
"campaignName": "Q1 SaaS Founders Outreach",
"totalLeads": 150,
"senderCount": 3,
"senders": [
{
"accountId": "cm3acc1",
"name": "John Smith"
},
{
"accountId": "cm3acc2",
"name": "Jane Doe"
},
{
"accountId": "cm3acc3",
"name": "Mike Wilson"
}
],
"stepCount": 5,
"launchedAt": "2026-06-01T08:00:00.000Z"
},
"timestamp": "2026-06-01T08:00:01.234Z",
"webhookId": "cm3wh789pqr"
}// When a campaign starts executing
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'campaign.launched') {
const { campaignName, totalLeads, senderCount } = data;
console.log(`Campaign "${campaignName}" launched!`);
console.log(`${totalLeads} leads across ${senderCount} senders`);
}
res.status(200).json({ received: true });
});campaign.step.completedCampaignsFired each time a campaign step is successfully executed for a lead. High-volume event — fires once per lead per step.
{
"event": "campaign.step.completed",
"data": {
"campaignId": "cm3abc123def456",
"campaignName": "Q1 SaaS Founders Outreach",
"executionId": "cm3exec789ghi",
"leadId": "cm3lead456jkl",
"lead": {
"firstName": "Sarah",
"lastName": "Chen",
"fullName": "Sarah Chen",
"company": "TechStartup Inc.",
"linkedinUrl": "https://linkedin.com/in/sarahchen"
},
"step": {
"type": "CONNECTION_REQUEST",
"order": 1,
"messageText": "Hi Sarah, I noticed we're both in the SaaS space..."
},
"accountId": "cm3acc123mno",
"accountName": "John Smith",
"executedAt": "2026-06-02T10:30:00.000Z"
},
"timestamp": "2026-06-02T10:30:01.567Z",
"webhookId": "cm3wh789pqr"
}// When a step executes for a lead
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'campaign.step.completed') {
const { lead, step, campaignName } = data;
console.log(`${step.type} completed for ${lead.fullName}`);
// Example: Log to analytics
await analytics.track('outreach_step', {
campaign: campaignName,
stepType: step.type,
lead: lead.fullName,
});
}
res.status(200).json({ received: true });
});campaign.step.failedCampaignsFired when a campaign step fails for a lead after all retry attempts. Includes error details for debugging.
{
"event": "campaign.step.failed",
"data": {
"campaignId": "cm3abc123def456",
"campaignName": "Q1 SaaS Founders Outreach",
"executionId": "cm3exec789ghi",
"leadId": "cm3lead456jkl",
"lead": {
"firstName": "Alex",
"lastName": "Rivera",
"fullName": "Alex Rivera",
"company": "StartupCo",
"linkedinUrl": "https://linkedin.com/in/alexrivera"
},
"step": {
"type": "MESSAGE",
"order": 3
},
"error": "Rate limit exceeded — account paused for 2 hours",
"accountId": "cm3acc123mno",
"accountName": "John Smith",
"failedAt": "2026-06-03T14:20:00.000Z"
},
"timestamp": "2026-06-03T14:20:01.890Z",
"webhookId": "cm3wh789pqr"
}// When a step fails for a lead
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'campaign.step.failed') {
const { lead, step, error, campaignName } = data;
console.error(`Step ${step.type} failed for ${lead.fullName}: ${error}`);
// Example: Alert on repeated failures
await pagerduty.createIncident({
title: `Campaign step failed: ${campaignName}`,
body: `${step.type} failed for ${lead.fullName}: ${error}`,
});
}
res.status(200).json({ received: true });
});account.status_changedAccountsFired when a LinkedIn account status changes — paused due to rate limiting, warned by LinkedIn, or restored to active.
{
"event": "account.status_changed",
"data": {
"accountId": "cm3acc123mno",
"accountName": "John Smith",
"previousStatus": "ACTIVE",
"newStatus": "WARNED",
"reason": "Unusual activity detected by LinkedIn",
"changedAt": "2026-06-04T11:00:00.000Z"
},
"timestamp": "2026-06-04T11:00:02.345Z",
"webhookId": "cm3wh789pqr"
}// When a LinkedIn account status changes
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'account.status_changed') {
const { accountName, previousStatus, newStatus, reason } = data;
console.log(`Account "${accountName}": ${previousStatus} → ${newStatus}`);
if (newStatus === 'WARNED' || newStatus === 'PAUSED') {
// Example: Send urgent Slack alert
await slack.chat.postMessage({
channel: '#linkedin-alerts',
text: `⚠️ Account "${accountName}" is now ${newStatus}!\nReason: ${reason}`,
});
}
}
res.status(200).json({ received: true });
});lead.createdLeadsFired when new leads are imported into the system — via search, CSV upload, or manual creation. Fires once per lead.
{
"event": "lead.created",
"data": {
"leadId": "cm3lead456jkl",
"lead": {
"firstName": "Emily",
"lastName": "Zhang",
"fullName": "Emily Zhang",
"headline": "Head of Growth at Acme Corp",
"company": "Acme Corp",
"email": "emily@acme.com",
"linkedinUrl": "https://linkedin.com/in/emilyzhang",
"location": "San Francisco, CA"
},
"listId": "cm3list789abc",
"listName": "SaaS Founders — West Coast",
"source": "SEARCH"
},
"timestamp": "2026-06-05T08:45:00.123Z",
"webhookId": "cm3wh789pqr"
}// When new leads are imported
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'lead.created') {
const { lead, listName, source } = data;
console.log(`New lead: ${lead.fullName} added to "${listName}" via ${source}`);
// Example: Enrich lead data
if (lead.email) {
await enrichment.lookup(lead.email);
}
// Example: Sync to CRM
await crm.contacts.create({
name: lead.fullName,
company: lead.company,
email: lead.email,
linkedinUrl: lead.linkedinUrl,
source: `Outreach - ${listName}`,
});
}
res.status(200).json({ received: true });
});Using with Zapier, Make, or n8n
These automation platforms can receive webhooks directly. Simply use their webhook trigger URL as your endpoint:
Zapier
Create a new Zap → Choose "Webhooks by Zapier" as trigger → Select "Catch Hook" → Copy the webhook URL → Paste it in Settings → API → Webhooks.
Make (Integromat)
Create a new scenario → Add "Webhooks" module → Select "Custom webhook" → Copy the URL → Paste it in Settings → API → Webhooks.
n8n
Add a "Webhook" trigger node → Set method to POST → Copy the production URL → Paste it in Settings → API → Webhooks.
Delivery Behavior
| Timeout | 10 seconds per delivery attempt |
| Success codes | Any 2xx status code (200, 201, 202, 204) |
| Auto-disable | After 10 consecutive failures, webhook is disabled |
| Delivery order | Best-effort chronological, not guaranteed |
| Idempotency | Use webhookId + timestamp to deduplicate |