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/webhooksResponse (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
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to receive webhooks |
events | array | Yes | Event types to subscribe to |
enabled | boolean | No | Enable webhook (default: true) |
description | string | No | Webhook 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:
| Event | Description |
|---|---|
lead.created | New lead received |
lead.updated | Lead data modified |
lead.deleted | Lead deleted |
lead.validated | Lead passed validation |
lead.rejected | Lead rejected by rules |
lead.duplicate | Duplicate lead detected |
lead.enriched | Enrichment rules applied |
lead.crm_synced | Lead synced to CRM |
lead.crm_sync_failed | CRM sync failed |
vendor.enabled | Vendor enabled |
vendor.disabled | Vendor disabled |
export.completed | Export ready for download |
export.failed | Export 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:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 24 hours |
After 5 failed attempts, the webhook is marked as failed and delivery stops.
Webhook Delivery Requirements
Your endpoint must:
- Respond within 30 seconds
- Return 2xx status code for successful delivery
- Use HTTPS (HTTP not supported)
- 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
- Check webhook is enabled
- Verify you’re subscribed to the correct event types
- Check the event is actually occurring (e.g., leads being created)
Deliveries Failing
- Check endpoint URL - Must be HTTPS and publicly accessible
- Verify response time - Must respond within 30 seconds
- Check status code - Must return 2xx
- Review logs - Check webhook delivery logs for error details
Signature Verification Failing
- Use raw body - Don’t parse JSON before verification
- Check secret - Ensure you’re using the correct webhook secret
- Verify timestamp - Check timestamp isn’t too old
High Latency
- Process asynchronously - Queue webhooks for background processing
- Optimize endpoint - Reduce processing time to < 1 second
- Scale infrastructure - Ensure endpoint can handle volume
Rate Limits
| Operation | Limit |
|---|---|
| List webhooks | 100 requests/minute |
| Create webhook | 10 requests/minute |
| Update webhook | 50 requests/minute |
| Test webhook | 20 requests/minute |
| Get deliveries | 100 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);
});Related Resources
- Lead Ingestion API - Submitting leads
- Webhooks Integration Guide - Detailed setup guide
- Events Reference - All available events