Webhooks API

Configure webhooks to receive real-time notifications when events occur in your Ledly account.

Base URL: https://api.ledly.io/api

Authentication: Bearer Token (see Authentication)


Overview

Webhooks allow your application to receive HTTP POST requests when specific events occur, such as:

  • New lead created
  • Lead updated
  • Lead synced to CRM
  • Lead rejected
  • Duplicate detected

Instead of polling the API, webhooks push data to your server in real-time.


List Webhooks

Retrieve all configured webhooks.

Endpoint: GET /api/webhooks

Request

curl -H "Authorization: Bearer your_token" \
  https://api.ledly.io/api/webhooks

Response (200 OK)

{
  "data": [
    {
      "id": "webhook_abc123",
      "url": "https://your-app.com/webhooks/ledly",
      "events": ["lead.created", "lead.updated"],
      "enabled": true,
      "secret": "whsec_abc123...",
      "created_at": "2024-12-01T10:00:00Z",
      "last_triggered_at": "2024-12-25T09:15:00Z",
      "stats": {
        "total_deliveries": 15234,
        "successful_deliveries": 15187,
        "failed_deliveries": 47,
        "last_30_days": 1523
      }
    }
  ]
}

Create Webhook

Create a new webhook subscription.

Endpoint: POST /api/webhooks

Request

curl -X POST https://api.ledly.io/api/webhooks \
  -H "Authorization: Bearer your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/ledly",
    "events": ["lead.created", "lead.updated", "lead.crm_synced"],
    "enabled": true,
    "description": "Production webhook for lead notifications"
  }'

Request Body

FieldTypeRequiredDescription
urlstringYesHTTPS URL to receive webhooks
eventsarrayYesEvent types to subscribe to
enabledbooleanNoEnable webhook (default: true)
descriptionstringNoWebhook description

Response (201 Created)

{
  "data": {
    "id": "webhook_xyz789",
    "url": "https://your-app.com/webhooks/ledly",
    "events": ["lead.created", "lead.updated", "lead.crm_synced"],
    "enabled": true,
    "secret": "whsec_abc123def456...",
    "created_at": "2024-12-25T12:00:00Z"
  }
}
⚠️

Save the webhook secret securely - it’s only shown once and needed to verify webhook signatures.


Update Webhook

Update an existing webhook configuration.

Endpoint: PATCH /api/webhooks/:id

Request

curl -X PATCH https://api.ledly.io/api/webhooks/webhook_abc123 \
  -H "Authorization: Bearer your_token" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["lead.created", "lead.rejected"],
    "enabled": true
  }'

Response (200 OK)

{
  "data": {
    "id": "webhook_abc123",
    "url": "https://your-app.com/webhooks/ledly",
    "events": ["lead.created", "lead.rejected"],
    "enabled": true,
    "updated_at": "2024-12-25T12:30:00Z"
  }
}

Delete Webhook

Delete a webhook permanently.

Endpoint: DELETE /api/webhooks/:id

Request

curl -X DELETE https://api.ledly.io/api/webhooks/webhook_abc123 \
  -H "Authorization: Bearer your_token"

Response (204 No Content)


Test Webhook

Send a test event to verify your endpoint.

Endpoint: POST /api/webhooks/:id/test

Request

curl -X POST https://api.ledly.io/api/webhooks/webhook_abc123/test \
  -H "Authorization: Bearer your_token"

Response (200 OK)

{
  "success": true,
  "delivered": true,
  "response_status": 200,
  "response_time_ms": 145,
  "test_event_id": "evt_test_abc123"
}

If delivery fails:

{
  "success": false,
  "delivered": false,
  "error": "Connection timeout",
  "response_status": null,
  "response_time_ms": 30000
}

Available Events

Subscribe to these event types:

EventDescription
lead.createdNew lead received
lead.updatedLead data modified
lead.deletedLead deleted
lead.validatedLead passed validation
lead.rejectedLead rejected by rules
lead.duplicateDuplicate lead detected
lead.enrichedEnrichment rules applied
lead.crm_syncedLead synced to CRM
lead.crm_sync_failedCRM sync failed
vendor.enabledVendor enabled
vendor.disabledVendor disabled
export.completedExport ready for download
export.failedExport generation failed

Webhook Payload

Payload Structure

All webhooks follow this format:

{
  "id": "evt_abc123xyz",
  "event": "lead.created",
  "timestamp": "2024-12-25T10:30:00Z",
  "organization_id": "org_123",
  "data": {
    // Event-specific data
  }
}

Event: lead.created

{
  "id": "evt_abc123",
  "event": "lead.created",
  "timestamp": "2024-12-25T10:30:00Z",
  "organization_id": "org_123",
  "data": {
    "lead": {
      "id": "lead_xyz789",
      "email": "[email protected]",
      "first_name": "John",
      "last_name": "Doe",
      "phone": "+15551234567",
      "program_interest": "MBA",
      "source": "website",
      "utm_source": "google",
      "utm_medium": "cpc",
      "utm_campaign": "spring-2025",
      "custom_fields": {
        "preferred_start": "Fall 2025"
      },
      "created_at": "2024-12-25T10:30:00Z"
    }
  }
}

Event: lead.updated

{
  "id": "evt_def456",
  "event": "lead.updated",
  "timestamp": "2024-12-25T11:00:00Z",
  "organization_id": "org_123",
  "data": {
    "lead": {
      "id": "lead_xyz789",
      "email": "[email protected]",
      "phone": "+15559999999",
      "updated_at": "2024-12-25T11:00:00Z"
    },
    "changes": {
      "phone": {
        "old": "+15551234567",
        "new": "+15559999999"
      }
    }
  }
}

Event: lead.rejected

{
  "id": "evt_ghi789",
  "event": "lead.rejected",
  "timestamp": "2024-12-25T10:30:00Z",
  "organization_id": "org_123",
  "data": {
    "lead": {
      "email": "[email protected]",
      "first_name": "Spam",
      "rejected_at": "2024-12-25T10:30:00Z"
    },
    "rejection_reason": "Disposable email address not allowed",
    "rule_id": "rule_reject_disposable"
  }
}

Event: lead.crm_synced

{
  "id": "evt_jkl012",
  "event": "lead.crm_synced",
  "timestamp": "2024-12-25T10:31:00Z",
  "organization_id": "org_123",
  "data": {
    "lead_id": "lead_xyz789",
    "crm_type": "salesforce",
    "crm_id": "00Q1234567890ABC",
    "crm_record_url": "https://your-instance.salesforce.com/00Q1234567890ABC",
    "synced_at": "2024-12-25T10:31:00Z"
  }
}

Event: export.completed

{
  "id": "evt_mno345",
  "event": "export.completed",
  "timestamp": "2024-12-25T12:00:00Z",
  "organization_id": "org_123",
  "data": {
    "export_id": "export_abc123",
    "format": "csv",
    "row_count": 1500,
    "download_url": "https://api.ledly.io/exports/export_abc123/download",
    "expires_at": "2024-12-26T12:00:00Z"
  }
}

Webhook Security

Verifying Signatures

Each webhook includes an X-Ledly-Signature header to verify authenticity.

Header Format:

X-Ledly-Signature: t=1703505600,v1=abc123def456...

Verification Example

const crypto = require('crypto');
 
function verifyWebhookSignature(payload, signature, secret) {
  // Extract timestamp and signature
  const parts = signature.split(',');
  const timestamp = parts[0].split('=')[1];
  const receivedSig = parts[1].split('=')[1];
 
  // Create signed payload
  const signedPayload = `${timestamp}.${payload}`;
 
  // Compute expected signature
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
 
  // Compare signatures
  return crypto.timingSafeEqual(
    Buffer.from(receivedSig),
    Buffer.from(expectedSig)
  );
}
 
// Express.js example
app.post('/webhooks/ledly', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-ledly-signature'];
  const payload = req.body.toString();
 
  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = JSON.parse(payload);
  console.log('Received event:', event.event);
 
  // Process webhook...
  res.status(200).send('OK');
});

Timestamp Validation

Prevent replay attacks by checking the timestamp:

function verifyTimestamp(signature, maxAge = 300) {
  const timestamp = parseInt(signature.split(',')[0].split('=')[1]);
  const now = Math.floor(Date.now() / 1000);
 
  // Reject if timestamp is older than maxAge seconds
  return (now - timestamp) <= maxAge;
}

Retry Policy

If your endpoint doesn’t return a 2xx status code, Ledly will retry delivery:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry24 hours

After 5 failed attempts, the webhook is marked as failed and delivery stops.

Webhook Delivery Requirements

Your endpoint must:

  1. Respond within 30 seconds
  2. Return 2xx status code for successful delivery
  3. Use HTTPS (HTTP not supported)
  4. Be publicly accessible (no localhost)

Webhook delivery is asynchronous. Process the webhook and respond quickly (< 1 second). For long-running tasks, queue the work and respond immediately.


Webhook Logs

View delivery history and debug failures.

Endpoint: GET /api/webhooks/:id/deliveries

Request

curl -H "Authorization: Bearer your_token" \
  "https://api.ledly.io/api/webhooks/webhook_abc123/deliveries?page=1&limit=20"

Response (200 OK)

{
  "data": [
    {
      "id": "delivery_abc123",
      "event_id": "evt_xyz789",
      "event_type": "lead.created",
      "status": "delivered",
      "response_status": 200,
      "response_time_ms": 145,
      "attempts": 1,
      "delivered_at": "2024-12-25T10:30:01Z",
      "next_retry_at": null
    },
    {
      "id": "delivery_def456",
      "event_id": "evt_uvw012",
      "event_type": "lead.updated",
      "status": "failed",
      "response_status": 500,
      "response_time_ms": 2341,
      "attempts": 3,
      "error": "Internal Server Error",
      "last_attempt_at": "2024-12-25T09:15:00Z",
      "next_retry_at": "2024-12-25T11:15:00Z"
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 150
  }
}

Best Practices

1. Verify Signatures

Always verify webhook signatures:

// ✅ Good
if (!verifySignature(payload, signature, secret)) {
  return res.status(401).send('Invalid signature');
}
 
// ❌ Bad - No verification
const event = req.body;
processEvent(event);

2. Return 200 Quickly

Process asynchronously and respond immediately:

// ✅ Good
app.post('/webhooks', (req, res) => {
  // Queue for processing
  queue.add('process-webhook', req.body);
 
  // Respond immediately
  res.status(200).send('OK');
});
 
// ❌ Bad - Slow processing
app.post('/webhooks', async (req, res) => {
  await processLead(req.body); // Might take 10+ seconds
  res.status(200).send('OK');
});

3. Handle Duplicates

Use event IDs to prevent duplicate processing:

const processedEvents = new Set();
 
app.post('/webhooks', (req, res) => {
  const event = req.body;
 
  if (processedEvents.has(event.id)) {
    return res.status(200).send('Already processed');
  }
 
  processedEvents.add(event.id);
  queue.add('process-webhook', event);
 
  res.status(200).send('OK');
});

4. Log Everything

Log all webhook deliveries for debugging:

app.post('/webhooks', (req, res) => {
  const event = req.body;
 
  logger.info('Webhook received', {
    eventId: event.id,
    eventType: event.event,
    timestamp: event.timestamp
  });
 
  // Process...
});

5. Monitor Failures

Set up alerts for failed webhooks:

if (failureRate > 0.05) {
  alertDevTeam('Webhook failure rate exceeds 5%');
}

Troubleshooting

Webhook Not Triggering

  1. Check webhook is enabled
  2. Verify you’re subscribed to the correct event types
  3. Check the event is actually occurring (e.g., leads being created)

Deliveries Failing

  1. Check endpoint URL - Must be HTTPS and publicly accessible
  2. Verify response time - Must respond within 30 seconds
  3. Check status code - Must return 2xx
  4. Review logs - Check webhook delivery logs for error details

Signature Verification Failing

  1. Use raw body - Don’t parse JSON before verification
  2. Check secret - Ensure you’re using the correct webhook secret
  3. Verify timestamp - Check timestamp isn’t too old

High Latency

  1. Process asynchronously - Queue webhooks for background processing
  2. Optimize endpoint - Reduce processing time to < 1 second
  3. Scale infrastructure - Ensure endpoint can handle volume

Rate Limits

OperationLimit
List webhooks100 requests/minute
Create webhook10 requests/minute
Update webhook50 requests/minute
Test webhook20 requests/minute
Get deliveries100 requests/minute

Examples

Complete Webhook Handler

const express = require('express');
const crypto = require('crypto');
const Queue = require('bull');
 
const app = express();
const webhookQueue = new Queue('webhooks');
 
// Use raw body for signature verification
app.post('/webhooks/ledly',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    try {
      // 1. Verify signature
      const signature = req.headers['x-ledly-signature'];
      const payload = req.body.toString();
 
      if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
      }
 
      // 2. Verify timestamp
      if (!verifyTimestamp(signature, 300)) {
        return res.status(401).send('Timestamp too old');
      }
 
      // 3. Parse event
      const event = JSON.parse(payload);
 
      // 4. Check for duplicates
      const processed = await checkIfProcessed(event.id);
      if (processed) {
        return res.status(200).send('Already processed');
      }
 
      // 5. Queue for processing
      await webhookQueue.add(event);
 
      // 6. Log receipt
      console.log(`Webhook received: ${event.event} (${event.id})`);
 
      // 7. Respond quickly
      res.status(200).send('OK');
 
    } catch (error) {
      console.error('Webhook error:', error);
      res.status(500).send('Internal error');
    }
  }
);
 
// Process webhooks in background
webhookQueue.process(async (job) => {
  const event = job.data;
 
  switch (event.event) {
    case 'lead.created':
      await handleLeadCreated(event.data.lead);
      break;
    case 'lead.updated':
      await handleLeadUpdated(event.data.lead, event.data.changes);
      break;
    case 'lead.crm_synced':
      await handleCrmSynced(event.data);
      break;
  }
 
  await markAsProcessed(event.id);
});