Set up real-time webhooks to sync SmartLead events with your CRM, database, or automation tools — including event types, payload formats, and best practices
Webhooks let SmartLead push real-time event notifications to your server whenever something happens in a campaign — a lead replies, an email bounces, someone unsubscribes, or a message is sent. Instead of polling the API for updates, you receive instant HTTP POST requests to your endpoint.This guide covers how to set up webhooks, handle events, verify payloads, and integrate with common tools like CRMs and Slack.
SmartLead supports three webhook scopes, set via the association_type field:
Level
association_type
Scope
Use Case
User
"user"
All campaigns owned by the user
Centralized notifications
Client
"client"
All campaigns for a specific client
Agency/white-label setups
Campaign
"campaign"
A single campaign
Per-campaign tracking
If a User-level webhook exists, it takes priority over Client and Campaign-level webhooks for the same event type. Keep this in mind when configuring webhooks at multiple levels.
Every webhook sends a JSON POST request. The payload is a flat JSON object with an event_type field identifying the event. The remaining fields vary by event type.
The LEAD_CATEGORY_UPDATED payload includes the full conversation history between the sending account and the lead, including all sent emails, replies, and threaded replies.
from flask import Flask, request, jsonifyapp = Flask(__name__)@app.route('/webhooks/smartlead', methods=['POST'])def handle_webhook(): event = request.json if event['event_type'] == 'EMAIL_REPLY': print(f"Reply from {event['to_email']}") # Sync to CRM, create task, notify Slack elif event['event_type'] == 'EMAIL_BOUNCE': print(f"Bounce: {event.get('to_email')}") # Flag in database, update lead quality score elif event['event_type'] == 'LEAD_UNSUBSCRIBED': print(f"Unsubscribed: {event['lead_email']}") # Add to suppression list elif event['event_type'] == 'LEAD_CATEGORY_UPDATED': print(f"{event['lead_email']} -> {event['category']}") # Sync category to CRM return jsonify({'received': True}), 200if __name__ == '__main__': app.run(port=3000)
Always return a 200 status code quickly. If your endpoint times out or returns an error, SmartLead will retry the webhook. Return 2xx for success, 4xx for permanent failures (no retry), 5xx for temporary failures (will retry).
Use the X-Request-Id header to prevent processing duplicate webhook deliveries:
Python
processed_events = set() # Use Redis or a database in production@app.route('/webhooks/smartlead', methods=['POST'])def handle_webhook(): request_id = request.headers.get('X-Request-Id', '') if request_id in processed_events: return jsonify({'status': 'already_processed'}), 200 processed_events.add(request_id) # Process event... return jsonify({'received': True}), 200
After 3 failed attempts, the event is marked as failed. Use the Retrigger Webhooks endpoint to manually retry failed deliveries, or view webhook statistics via the Webhook Summary endpoint.Response code behavior:
Verify your endpoint is publicly accessible (not localhost). Check that the webhook is active. Ensure your endpoint returns a 200 response within 30 seconds. Check server logs for incoming requests. Use a tool like webhook.site for testing.
Receiving duplicate events
SmartLead may retry if your server didn’t respond with 200 in time. Implement idempotency by checking the X-Request-Id header — skip events you’ve already processed.
Webhook payloads are missing fields
Not all event types include all fields. For example, reply_body and preview_text are only present on EMAIL_REPLY events. The LEAD_UNSUBSCRIBED event uses lead_email instead of to_email. Always check for field existence before accessing.
User-level webhook overriding campaign webhook
If a User-level webhook exists for the same event type, it takes priority over Client and Campaign-level webhooks. Check your webhook configuration at all levels.