Webhooks #
Configure webhook endpoints to track email events in real-time from SMTP providers.
Overview #
Webhook integration provides:
- Real-time Tracking: Instant email event notifications
- Multiple Providers: SendGrid, Mailgun, Postmark, Amazon SES, Brevo
- Event Processing: Delivered, opened, clicked, bounced, complained, unsubscribed
- Signature Verification: Secure webhook validation
- Automatic Stats: Updates message statistics automatically
Database Structure #
Schema::create('campaign_webhook_endpoints', function($table) {
$table->increments('id');
$table->integer('organization_id')->unsigned();
$table->string('provider'); // sendgrid, mailgun, postmark, ses, brevo
$table->string('name');
$table->string('url');
$table->string('secret')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('campaign_webhook_events', function($table) {
$table->increments('id');
$table->integer('webhook_endpoint_id')->unsigned();
$table->integer('message_id')->unsigned()->nullable();
$table->integer('subscriber_id')->unsigned()->nullable();
$table->string('event_type'); // sent, delivered, opened, clicked, etc.
$table->json('payload');
$table->timestamp('event_at');
$table->timestamps();
});
Webhook Providers #
SendGrid #
// Webhook URL
https://yourdomain.com/api/campaign/webhook/sendgrid
// Events supported
- sent
- delivered
- open
- click
- bounce
- dropped
- spamreport
- unsubscribe
Mailgun #
// Webhook URL
https://yourdomain.com/api/campaign/webhook/mailgun
// Events supported
- delivered
- opened
- clicked
- bounced
- complained
- unsubscribed
Postmark #
// Webhook URL
https://yourdomain.com/api/campaign/webhook/postmark
// Events supported
- Delivery
- Open
- Click
- Bounce
- SpamComplaint
Amazon SES #
// Webhook URL (via SNS)
https://yourdomain.com/api/campaign/webhook/ses
// Events supported
- Delivery
- Open
- Click
- Bounce
- Complaint
Brevo (Sendinblue) #
// Webhook URL
https://yourdomain.com/api/campaign/webhook/brevo
// Events supported
- delivered
- opened
- clicked
- hard_bounce
- soft_bounce
- unsubscribed
Webhook Processing #
WebhookProcessor Class #
Location: Classes/WebhookProcessor.php
use Albrightlabs\Campaign\Classes\WebhookProcessor;
$processor = new WebhookProcessor();
$processor->processWebhook($provider, $payload);
Event Mapping #
public function processWebhook($provider, $payload)
{
// Extract event data
$eventData = $this->parseProviderPayload($provider, $payload);
// Find message and subscriber
$message = $this->findMessage($eventData);
$subscriber = $this->findSubscriber($eventData);
// Update statistics
$this->updateStatistics($message, $subscriber, $eventData['event']);
// Log event
$this->logWebhookEvent($eventData);
}
Provider Configuration #
SendGrid Setup #
- Log into SendGrid Dashboard
- Navigate to Settings → Mail Settings → Event Webhook
- Configure:
URL: https://yourdomain.com/api/campaign/webhook/sendgrid HTTP POST Select events: All
Mailgun Setup #
- Log into Mailgun Control Panel
- Navigate to Webhooks
- Add webhooks for each event type:
URL: https://yourdomain.com/api/campaign/webhook/mailgun
Postmark Setup #
- Log into Postmark Dashboard
- Navigate to Servers → Webhooks
- Configure:
Webhook URL: https://yourdomain.com/api/campaign/webhook/postmark Select events: All
Signature Verification #
SendGrid #
private function verifySignature($payload, $signature, $secret)
{
$expected = base64_encode(hash_hmac(
'sha256',
$payload . $timestamp . $signature,
$secret,
true
));
return hash_equals($expected, $signature);
}
Mailgun #
private function verifyMailgunSignature($token, $timestamp, $signature, $secret)
{
$expected = hash_hmac('sha256', $timestamp . $token, $secret);
return hash_equals($expected, $signature);
}
Statistics Updates #
Event Processing #
switch ($eventType) {
case 'delivered':
$message->count_delivered++;
break;
case 'opened':
$message->count_read++;
break;
case 'clicked':
$message->count_clicked++;
break;
case 'bounced':
$message->count_bounced++;
$subscriber->status_id = SubscriberStatus::STATUS_BOUNCED;
break;
case 'unsubscribed':
$message->count_stop++;
$subscriber->status_id = SubscriberStatus::STATUS_UNSUBSCRIBED;
$subscriber->unsubscribed_at = now();
break;
}
$message->save();
$subscriber->save();
Webhook Management #
Creating Endpoints #
use Albrightlabs\Campaign\Models\OrganizationWebhook;
$webhook = OrganizationWebhook::create([
'organization_id' => $org->id,
'provider' => 'sendgrid',
'name' => 'SendGrid Production',
'url' => 'https://yourdomain.com/api/campaign/webhook/sendgrid',
'secret' => 'webhook-secret-key',
'is_active' => true
]);
Testing Webhooks #
// Test webhook delivery
curl -X POST https://yourdomain.com/api/campaign/webhook/sendgrid \
-H "Content-Type: application/json" \
-d @test-payload.json
Viewing Logs #
// Get recent webhook events
$events = WebhookEvent::where('webhook_endpoint_id', $webhookId)
->orderBy('event_at', 'desc')
->limit(100)
->get();
Backend UI #
Webhook Settings #
Location: Settings → Webhooks
Backend::url('albrightlabs/campaign/webhooksettings')
Configuration Form #
# fields.yaml
fields:
provider:
label: Provider
type: dropdown
options:
sendgrid: SendGrid
mailgun: Mailgun
postmark: Postmark
ses: Amazon SES
brevo: Brevo
name:
label: Webhook Name
required: true
url:
label: Webhook URL
disabled: true
comment: Copy this URL to your provider's webhook settings
secret:
label: Webhook Secret
comment: Used for signature verification
is_active:
label: Active
type: switch
Troubleshooting #
Webhooks Not Triggering #
// Check webhook configuration
$webhook = OrganizationWebhook::find($id);
echo "Active: " . ($webhook->is_active ? 'Yes' : 'No');
echo "URL: " . $webhook->url;
// Test with provider's webhook tester
// Check firewall allows incoming webhooks
// Verify URL is publicly accessible
Statistics Not Updating #
// Check recent webhook events
$events = WebhookEvent::where('message_id', $messageId)
->orderBy('event_at', 'desc')
->get();
if ($events->isEmpty()) {
// No events received - check provider config
}
// Manually rebuild stats
$message->rebuildStats();
Signature Verification Failures #
// Verify secret matches
$webhook = OrganizationWebhook::where('provider', $provider)->first();
echo "Secret: " . $webhook->secret;
// Check payload format
// Ensure timestamp within tolerance
// Verify signature algorithm matches provider