Weezly Outreach
|Developer Docs

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:

HeaderDescription
Content-TypeAlways application/json
X-Webhook-SignatureHMAC-SHA256 signature: t=timestamp,v1=hash
X-Webhook-EventThe event type, e.g. lead.replied
User-AgentAlways 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.

Node.js — Verify signatureJavaScript
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 });
});
Python — Verify signaturePython
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), 200

Payload Structure

Every webhook payload follows the same top-level structure. The data field contains event-specific fields documented below.

Base payload structureJSON
{
  "event": "event.type",
  "data": {
    "...event-specific fields": "..."
  },
  "timestamp": "2026-08-20T14:32:01.234Z",
  "webhookId": "cm3wh789pqr"
}

Test deliveries: when you use the test button (or the test API endpoint), the payload carries an extra top-level "test": true field so your handler can tell test deliveries apart from real events. Real deliveries never include it.

Campaign Step Types

The step.type field in campaign.step.completed and campaign.step.failed events will be one of:

TypeDescription
CONNECTION_REQUESTConnection Request
MESSAGEDirect Message
VIDEO_MESSAGEVideo Message
VOICE_NOTEVoice Note
INMAILInMail
VIEW_PROFILEProfile View
LIKE_POSTLike Post
REACT_TO_POSTReact to Post
COMMENT_ON_POSTComment on Post
FOLLOW_PROFILEFollow Profile
IF_CONNECTIONCondition branch: checks whether the lead is connected
IF_OPEN_PROFILECondition branch: checks whether the lead has an Open Profile
ENDEnd of sequence (or of a branch)

Event Reference

lead.repliedCampaigns

Fired when a lead sends a reply message to your outreach. This event triggers immediately when the reply is detected. The disposition field tells you what happened to the sequence: "stopped" (default), "stopped_negative" (the reply was a rejection), or "continued_ack" (the campaign has smart reply handling on and the reply was a quick acknowledgment, so the sequence keeps running). Deliveries with disposition "continued_ack" carry the same identifiers and message object but may omit the full lead object.

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "lead.replied",
  "data": {
    "campaignId": "cm3abc123def456",
    "campaignName": "Q1 SaaS Founders Outreach",
    "executionId": "cm3exec789ghi",
    "leadId": "cm3lead456jkl",
    "leadName": "Sarah Chen",
    "disposition": "stopped",
    "messagePreview": "Hi John, thanks for reaching out! I'd love to chat about this.",
    "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-08-20T14:32:00.000Z"
    }
  },
  "timestamp": "2026-08-20T14:32:01.234Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// 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 });
});
lead.interestedCampaigns

Fired when AI sentiment analysis detects that a reply shows genuine interest, with a confidence of at least 0.7 (70%). Always fired in addition to lead.replied for the same reply. Includes the AI's confidence score, its reasoning, and the reply message itself.

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "lead.interested",
  "data": {
    "campaignId": "cm3abc123def456",
    "campaignName": "Q1 SaaS Founders Outreach",
    "executionId": "cm3exec789ghi",
    "leadId": "cm3lead456jkl",
    "leadName": "Sarah Chen",
    "confidence": 0.92,
    "reasoning": "Lead explicitly requested a meeting and showed strong buying intent.",
    "messagePreview": "Yes, let's definitely connect. Can you send a calendar link?",
    "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": "Yes, let's definitely connect. Can you send a calendar link?",
      "chatId": "chat_abc123def456",
      "receivedAt": "2026-08-20T14:32:00.000Z"
    }
  },
  "timestamp": "2026-08-20T14:32:02.456Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// When AI detects an interested reply
app.post('/webhook', (req, res) => {
  const { event, data } = req.body;

  if (event === 'lead.interested') {
    const { lead, message, confidence, reasoning, campaignName } = data;

    console.log(`${lead.fullName} looks interested (${Math.round(confidence * 100)}% confident)`);
    console.log(`Reply: ${message.text}`);
    console.log(`AI reasoning: ${reasoning}`);

    // Example: Alert sales immediately
    await slack.chat.postMessage({
      channel: '#hot-leads',
      text: `🔥 ${lead.fullName} (${lead.company}) is interested in "${campaignName}"!\n> ${message.text}`,
    });

    // Example: Create a deal in your CRM
    await crm.deals.create({
      contact: lead.fullName,
      company: lead.company,
      stage: 'interested',
      notes: reasoning,
    });
  }

  res.status(200).json({ received: true });
});
meeting.bookedMeetings

Fired when a lead books a meeting through a Weezly booking link shared from the inbox. The link carries a per-conversation tracking token, so the booking attributes to the exact sender and conversation even when several senders share the same booking page. Includes the scheduled meeting time and who booked.

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "meeting.booked",
  "data": {
    "contactName": "Sarah Chen",
    "inviteeName": "Sarah Chen",
    "inviteeEmail": "sarah@techstartup.com",
    "meetingName": "45 min Product Demo",
    "meetingTime": "2026-08-26T14:00:00.000Z",
    "duration": "45 mins",
    "bookedAt": "2026-08-23T09:12:00.000Z",
    "accountName": "John Smith",
    "accountId": "cm3acc123mno",
    "chatId": "chat_abc123def456"
  },
  "timestamp": "2026-08-23T09:12:01.234Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
app.post('/webhook', (req, res) => {
  const { contactName, inviteeName, inviteeEmail, meetingName } = req.body.data;
  console.log('Event received:', req.body.event);
  res.status(200).json({ received: true });
});
connection.acceptedCampaigns

Fired when a lead accepts your LinkedIn connection request. Includes the lead's profile data and the campaign context.

Example payload
POSTyour-endpoint.com/webhook
{
  "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-08-20T09:15:00.000Z"
  },
  "timestamp": "2026-08-20T09:15:02.456Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// 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.completedCampaigns

Fired when all leads in a campaign have reached a terminal state (completed, replied, or failed). Includes final lead counts.

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "campaign.completed",
  "data": {
    "campaignName": "Q1 SaaS Founders Outreach",
    "totalLeads": 150,
    "leadsCompleted": 120,
    "leadsReplied": 18
  },
  "timestamp": "2026-08-20T16:00:05.789Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// When a campaign finishes all leads
app.post('/webhook', (req, res) => {
  const { event, data } = req.body;

  if (event === 'campaign.completed') {
    const { campaignName, totalLeads, leadsCompleted, leadsReplied } = data;

    console.log(`Campaign "${campaignName}" completed!`);
    console.log(`Results: ${leadsReplied} replies from ${totalLeads} leads`);

    // Example: Send summary email
    await email.send({
      to: 'team@company.com',
      subject: `Campaign "${campaignName}" finished: ${leadsReplied} replies`,
      body: `Completed ${leadsCompleted} of ${totalLeads} leads with ${leadsReplied} replies.`,
    });
  }

  res.status(200).json({ received: true });
});
campaign.launchedCampaigns

Fired when a campaign is launched and begins execution. Includes the total number of enrolled leads and the sender count.

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "campaign.launched",
  "data": {
    "campaignName": "Q1 SaaS Founders Outreach",
    "totalLeads": 150,
    "senderCount": 3
  },
  "timestamp": "2026-08-01T08:00:01.234Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// 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.completedCampaigns

Fired each time a campaign step is successfully executed for a lead. High-volume event, fires once per lead per step.

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "campaign.step.completed",
  "data": {
    "campaignId": "cm3abc123def456",
    "campaignName": "Q1 SaaS Founders Outreach",
    "executionId": "cm3exec789ghi",
    "leadId": "cm3lead456jkl",
    "leadName": "Sarah Chen",
    "stepType": "CONNECTION_REQUEST",
    "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": "PENDING",
      "source": "SEARCH"
    },
    "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-08-02T10:30:00.000Z"
  },
  "timestamp": "2026-08-02T10:30:01.567Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// 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.failedCampaigns

Fired when a campaign step fails for a lead after all retry attempts. Includes error details for debugging.

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "campaign.step.failed",
  "data": {
    "campaignId": "cm3abc123def456",
    "campaignName": "Q1 SaaS Founders Outreach",
    "executionId": "cm3exec789ghi",
    "leadId": "cm3lead456jkl",
    "leadName": "Alex Rivera",
    "stepType": "MESSAGE",
    "lead": {
      "id": "cm3lead456jkl",
      "firstName": "Alex",
      "lastName": "Rivera",
      "fullName": "Alex Rivera",
      "headline": "Founder at StartupCo",
      "jobTitle": "Founder",
      "company": "StartupCo",
      "email": null,
      "phone": null,
      "location": "Denver, CO",
      "linkedinUrl": "https://linkedin.com/in/alexrivera",
      "linkedinPublicId": "alexrivera",
      "photoUrl": null,
      "connectionStatus": "CONNECTED",
      "source": "CSV"
    },
    "step": {
      "type": "MESSAGE",
      "order": 3
    },
    "error": "Rate limit exceeded, account paused for 2 hours",
    "accountId": "cm3acc123mno",
    "accountName": "John Smith",
    "failedAt": "2026-08-03T14:20:00.000Z"
  },
  "timestamp": "2026-08-03T14:20:01.890Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// 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_changedAccounts

Fired when a LinkedIn account status changes (paused due to rate limiting, warned by LinkedIn, or restored to active).

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "account.status_changed",
  "data": {
    "accountName": "John Smith",
    "previousStatus": "ACTIVE",
    "newStatus": "WARNED",
    "reason": "Unusual activity detected by LinkedIn"
  },
  "timestamp": "2026-08-04T11:00:02.345Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// 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.createdLeads

Fired when a brand-new lead record is created in your account. Fires exactly once per lead; the source field tells you how the lead was created.

Example payload
POSTyour-endpoint.com/webhook
{
  "event": "lead.created",
  "data": {
    "leadId": "cm3lead456jkl",
    "leadName": "Emily Zhang",
    "lead": {
      "id": "cm3lead456jkl",
      "firstName": "Emily",
      "lastName": "Zhang",
      "fullName": "Emily Zhang",
      "headline": "Head of Growth at Acme Corp",
      "jobTitle": "Head of Growth",
      "company": "Acme Corp",
      "email": "emily@acme.com",
      "phone": null,
      "location": "San Francisco, CA",
      "linkedinUrl": "https://linkedin.com/in/emilyzhang",
      "linkedinPublicId": "emilyzhang",
      "photoUrl": "https://media.licdn.com/.../emily.jpg",
      "connectionStatus": null,
      "source": "ENRICHMENT"
    },
    "source": "ENRICHMENT"
  },
  "timestamp": "2026-08-05T08:45:00.123Z",
  "webhookId": "cm3wh789pqr"
}
Handle this eventNode.js
// When a new lead is created
app.post('/webhook', (req, res) => {
  const { event, data } = req.body;

  if (event === 'lead.created') {
    const { lead, source } = data;

    console.log(`New lead: ${lead.fullName} created 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 - ${source}`,
    });
  }

  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

Timeout10 seconds per delivery attempt
Success codesAny 2xx status code (200, 201, 202, 204)
Auto-disableAfter 10 consecutive failures, webhook is disabled
Delivery orderBest-effort chronological, not guaranteed
IdempotencyUse webhookId + timestamp to deduplicate