Black Friday: 50% OFF with code BF2025!Sign Up

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_KEY

Webhook Endpoints

Create Webhook

Create a new webhook subscription to start receiving event notifications.

POST https://api.campaignkit.cc/v1/webhooks

Request Body:

{
  "url": "https://your-domain.com/webhooks/campaignkit",
  "events": ["validation.job.completed", "credits.threshold.crossed"]
}
ParameterTypeRequiredDescription
urlstringYesThe URL to send webhook notifications to
eventsarrayYesArray 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/webhooks

Response:

[
  {
    "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:

EventDescription
validation.job.completedTriggered when a bulk validation job finishes (successfully or with errors)
credits.threshold.crossedTriggered 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

FieldTypeDescription
eventstringEvent type (always “validation.job.completed”)
job_idintegerUnique validation job ID
account_idintegerYour account ID
labelstringLabel assigned to the validation job
statestringJob state: “done” or “failed”
email_countintegerTotal number of emails in the job
deliverable_countintegerNumber of deliverable emails
undeliverable_countintegerNumber of undeliverable emails
risky_countintegerNumber of risky emails
unique_emails_countintegerNumber of unique emails (after deduplication)
credits_usedintegerNumber of validation credits consumed
created_atstringJob creation timestamp (ISO 8601)
finished_atstringJob completion timestamp (ISO 8601)
sourcestringSource 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

FieldTypeDescription
eventstringEvent type (always “credits.threshold.crossed”)
account_idintegerYour account ID
account_emailstringYour account email address
threshold_typestringType of threshold crossed: “low”, “critical”, or “depleted”
credits_remainingintegerCurrent number of credits remaining in your account
threshold_valueintegerThe threshold value that was crossed (0 for depleted)
alerted_atstringAlert timestamp (ISO 8601)
top_up_urlstringDirect link to purchase more credits
usage_stats_urlstringLink 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: 8f3e4d2c1b0a9e8d7c6b5a4e3d2c1b0a9e8d7c6b5a4e3d2c1b0a9e8d7c6b5a4e

The 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}), 200

Verification 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:

  1. Respond quickly: Return a 200 OK response within 10 seconds
  2. Process asynchronously: Handle the payload in a background job if processing takes longer
  3. 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/campaignkit

Webhook 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

  1. Verify your webhook URL is publicly accessible
  2. Check that your endpoint returns 200 OK within 10 seconds
  3. Ensure your firewall allows incoming requests from CampaignKit
  4. Review webhook logs in the CampaignKit dashboard

Invalid Signature Errors

  1. Verify you’re using the correct webhook secret from the webhook creation response
  2. Ensure you’re computing the HMAC on the raw request body (not parsed JSON)
  3. Check that you’re using SHA256 algorithm
  4. 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: