Webhooks
Webhooks allow you to receive real-time notifications when validation jobs are completed. Instead of polling the API for job status, CampaignKit will send an HTTP POST request to your specified URL when events occur.
What Are Webhooks?
Webhooks are automated HTTP callbacks triggered by specific events. When a validation job completes, CampaignKit sends a POST request to your webhook URL with details about the job results.
Use Cases
- Automated workflows: Trigger downstream processes when validations complete
- Real-time notifications: Alert your team when large batch validations finish
- Data synchronization: Update your database with validation results automatically
- Integration building: Connect CampaignKit with other systems in your infrastructure
Authentication
All webhook endpoints require authentication using your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEYWebhook Endpoints
Create Webhook
Create a new webhook subscription to start receiving event notifications.
POST https://api.campaignkit.cc/v1/webhooksRequest Body:
{
"url": "https://your-domain.com/webhooks/campaignkit",
"events": ["validation.job.completed", "credits.threshold.crossed"]
}| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The URL to send webhook notifications to |
events | array | Yes | Array of event types to subscribe to |
Response:
{
"id": 123,
"account_id": 456,
"url": "https://your-domain.com/webhooks/campaignkit",
"secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"events": ["validation.job.completed", "credits.threshold.crossed"],
"active": true,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}Store the secret value securely. You’ll need it to verify webhook signatures and ensure requests are coming from CampaignKit.
List Webhooks
Get all webhook subscriptions for your account.
GET https://api.campaignkit.cc/v1/webhooksResponse:
[
{
"id": 123,
"account_id": 456,
"url": "https://your-domain.com/webhooks/campaignkit",
"secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"events": ["validation.job.completed"],
"active": true,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
]Get Webhook
Retrieve a specific webhook by ID.
GET https://api.campaignkit.cc/v1/webhooks/{id}Response:
{
"id": 123,
"account_id": 456,
"url": "https://your-domain.com/webhooks/campaignkit",
"secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"events": ["validation.job.completed", "credits.threshold.crossed"],
"active": true,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}Update Webhook
Update an existing webhook subscription.
PUT https://api.campaignkit.cc/v1/webhooks/{id}Request Body:
{
"url": "https://your-domain.com/webhooks/new-endpoint",
"events": ["validation.job.completed", "credits.threshold.crossed"],
"active": true
}All fields are optional. Only include fields you want to update.
Response:
{
"id": 123,
"account_id": 456,
"url": "https://your-domain.com/webhooks/new-endpoint",
"secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"events": ["validation.job.completed"],
"active": true,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}Delete Webhook
Delete a webhook subscription.
DELETE https://api.campaignkit.cc/v1/webhooks/{id}Response: 204 No Content on success
Event Types
CampaignKit currently supports the following webhook events:
| Event | Description |
|---|---|
validation.job.completed | Triggered when a bulk validation job finishes (successfully or with errors) |
credits.threshold.crossed | Triggered when account credits fall below configured thresholds (low, critical, or reach zero) |
More event types will be added in future updates.
Webhook Payload
When an event occurs, CampaignKit sends a POST request to your webhook URL with the following payload:
validation.job.completed Event
{
"event": "validation.job.completed",
"job_id": 123,
"account_id": 456,
"label": "My Email List",
"state": "done",
"email_count": 1000,
"deliverable_count": 850,
"undeliverable_count": 100,
"risky_count": 50,
"unique_emails_count": 950,
"credits_used": 950,
"created_at": "2025-01-15T10:00:00Z",
"finished_at": "2025-01-15T10:15:00Z",
"source": "api"
}Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | Event type (always “validation.job.completed”) |
job_id | integer | Unique validation job ID |
account_id | integer | Your account ID |
label | string | Label assigned to the validation job |
state | string | Job state: “done” or “failed” |
email_count | integer | Total number of emails in the job |
deliverable_count | integer | Number of deliverable emails |
undeliverable_count | integer | Number of undeliverable emails |
risky_count | integer | Number of risky emails |
unique_emails_count | integer | Number of unique emails (after deduplication) |
credits_used | integer | Number of validation credits consumed |
created_at | string | Job creation timestamp (ISO 8601) |
finished_at | string | Job completion timestamp (ISO 8601) |
source | string | Source of the validation job (e.g., “api”, “zapier”, “dashboard”) |
credits.threshold.crossed Event
Triggered when your account’s credit balance falls below a configured threshold. This helps you proactively monitor your credit usage and avoid service interruptions.
{
"event": "credits.threshold.crossed",
"account_id": 456,
"account_email": "user@example.com",
"threshold_type": "low",
"credits_remaining": 20,
"threshold_value": 50,
"alerted_at": "2025-01-15T10:00:00Z",
"top_up_url": "https://app.campaignkit.cc/plans",
"usage_stats_url": "https://app.campaignkit.cc/stats"
}Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | Event type (always “credits.threshold.crossed”) |
account_id | integer | Your account ID |
account_email | string | Your account email address |
threshold_type | string | Type of threshold crossed: “low”, “critical”, or “depleted” |
credits_remaining | integer | Current number of credits remaining in your account |
threshold_value | integer | The threshold value that was crossed (0 for depleted) |
alerted_at | string | Alert timestamp (ISO 8601) |
top_up_url | string | Direct link to purchase more credits |
usage_stats_url | string | Link to view your usage statistics |
Use Cases
Credit alert webhooks enable you to:
- Automate notifications: Send alerts to Slack, email, or PagerDuty when credits run low
- Trigger auto-recharge: Automatically purchase more credits when balance drops below a threshold
- Monitor usage patterns: Track credit consumption across multiple accounts or projects
- Prevent service disruption: Get proactive warnings before running out of credits
Example: Slack Notification
app.post('/webhooks/campaignkit', express.json(), (req, res) => {
const { event, threshold_type, credits_remaining } = req.body;
if (event === 'credits.threshold.crossed') {
const emoji = threshold_type === 'critical' ? '🚨' : '⚠️';
const message = `${emoji} CampaignKit Credits ${threshold_type.toUpperCase()}: ${credits_remaining} credits remaining`;
// Send to Slack
fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: message,
blocks: [{
type: 'section',
text: { type: 'mrkdwn', text: message }
}, {
type: 'actions',
elements: [{
type: 'button',
text: { type: 'plain_text', text: 'Top Up Credits' },
url: req.body.top_up_url
}]
}]
})
});
}
res.status(200).json({ received: true });
});Webhook Security
Signature Verification
Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature. Verify this signature to ensure the request is from CampaignKit and hasn’t been tampered with.
Header:
X-Webhook-Signature: 8f3e4d2c1b0a9e8d7c6b5a4e3d2c1b0a9e8d7c6b5a4e3d2c1b0a9e8d7c6b5a4eThe signature is computed as:
HMAC-SHA256(webhook_secret, request_body)Verification Example (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(body, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js middleware example
app.post('/webhooks/campaignkit', express.json(), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const secret = process.env.CAMPAIGNKIT_WEBHOOK_SECRET;
if (!verifyWebhookSignature(req.body, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the webhook
const { event, job_id, state } = req.body;
if (event === 'validation.job.completed' && state === 'done') {
console.log(`Job ${job_id} completed successfully`);
// Handle the completed job...
}
res.status(200).json({ received: true });
});Verification Example (Python)
import hmac
import hashlib
import json
def verify_webhook_signature(body, signature, secret):
expected_signature = hmac.new(
secret.encode(),
json.dumps(body).encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/campaignkit', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature')
secret = os.environ['CAMPAIGNKIT_WEBHOOK_SECRET']
if not verify_webhook_signature(request.json, signature, secret):
return jsonify({'error': 'Invalid signature'}), 401
data = request.json
event = data['event']
job_id = data['job_id']
if event == 'validation.job.completed' and data['state'] == 'done':
print(f"Job {job_id} completed successfully")
# Handle the completed job...
return jsonify({'received': True}), 200Verification Example (PHP)
<?php
function verifyWebhookSignature($body, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', $body, $secret);
return hash_equals($signature, $expectedSignature);
}
// Get raw POST body
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
$secret = getenv('CAMPAIGNKIT_WEBHOOK_SECRET');
if (!verifyWebhookSignature($body, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$data = json_decode($body, true);
if ($data['event'] === 'validation.job.completed' && $data['state'] === 'done') {
error_log("Job {$data['job_id']} completed successfully");
// Handle the completed job...
}
http_response_code(200);
echo json_encode(['received' => true]);
?>Always verify the webhook signature before processing the payload. This prevents attackers from sending fake webhook requests to your endpoint.
Webhook Response Requirements
Your webhook endpoint must:
- Respond quickly: Return a
200 OKresponse within 10 seconds - Process asynchronously: Handle the payload in a background job if processing takes longer
- Be idempotent: Handle duplicate webhook deliveries gracefully (same event may be sent multiple times)
Example Response
app.post('/webhooks/campaignkit', (req, res) => {
// Immediately acknowledge receipt
res.status(200).json({ received: true });
// Process webhook asynchronously
processWebhookAsync(req.body).catch(err => {
console.error('Webhook processing error:', err);
});
});Retry Logic
If your webhook endpoint doesn’t respond with a 2xx status code, CampaignKit will retry the delivery:
- Retry attempts: Up to 5 attempts
- Retry schedule: Exponential backoff (1 min, 5 min, 30 min, 2 hours, 12 hours)
- Timeout: 10 seconds per attempt
After 5 failed attempts, the webhook will be marked as failed and no further retries will be attempted for that event.
Monitor your webhook endpoint logs to ensure it’s responding successfully. Repeated failures may result in your webhook being automatically disabled.
Testing Webhooks
Local Testing with Ngrok
Use ngrok to test webhooks locally during development:
# Start your local server
npm start
# In another terminal, start ngrok
ngrok http 3000
# Use the ngrok URL in your webhook configuration
# Example: https://abc123.ngrok.io/webhooks/campaignkitWebhook Testing Tools
- Webhook.site - Inspect incoming webhooks in real-time
- RequestBin - Collect and inspect webhook payloads
- Ngrok - Expose your local server to the internet
Test Webhook Creation
Create a test webhook to verify your implementation:
curl -X POST https://api.campaignkit.cc/v1/webhooks \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhooks/campaignkit",
"events": ["validation.job.completed", "credits.threshold.crossed"]
}'Then trigger a small validation job and verify your endpoint receives the webhook.
Best Practices
Use HTTPS
Always use HTTPS URLs for your webhook endpoints to ensure data is encrypted in transit.
Implement Idempotency
Store processed webhook IDs to avoid processing the same event multiple times:
const processedWebhooks = new Set();
app.post('/webhooks/campaignkit', (req, res) => {
const { job_id, event } = req.body;
const webhookId = `${event}-${job_id}`;
if (processedWebhooks.has(webhookId)) {
return res.status(200).json({ received: true, duplicate: true });
}
processedWebhooks.add(webhookId);
// Process the webhook...
res.status(200).json({ received: true });
});Monitor Webhook Health
Log webhook deliveries and failures to monitor the health of your integration:
app.post('/webhooks/campaignkit', (req, res) => {
logger.info('Webhook received', {
event: req.body.event,
job_id: req.body.job_id,
timestamp: new Date().toISOString()
});
// Process webhook...
});Handle Failures Gracefully
If your webhook processing fails, log the error but still return 200 OK to acknowledge receipt:
app.post('/webhooks/campaignkit', async (req, res) => {
// Acknowledge immediately
res.status(200).json({ received: true });
try {
await processWebhook(req.body);
} catch (error) {
logger.error('Webhook processing failed', {
error: error.message,
job_id: req.body.job_id
});
// Store failed webhook for manual retry
await storeFailedWebhook(req.body, error);
}
});Troubleshooting
Webhook Not Receiving Requests
- Verify your webhook URL is publicly accessible
- Check that your endpoint returns
200 OKwithin 10 seconds - Ensure your firewall allows incoming requests from CampaignKit
- Review webhook logs in the CampaignKit dashboard
Invalid Signature Errors
- Verify you’re using the correct webhook secret from the webhook creation response
- Ensure you’re computing the HMAC on the raw request body (not parsed JSON)
- Check that you’re using SHA256 algorithm
- Verify the signature comparison is using a timing-safe equality function
Duplicate Webhook Deliveries
This is expected behavior. Implement idempotency checks using the job_id to handle duplicates.
Need Help?
If you encounter any issues with webhooks:
- Check the API Overview for authentication and general information
- Review Email Validation documentation for validation endpoint details
- Contact support at support@campaignkit.cc