Webhooks
Webhooks allow your application to receive real-time notifications when validation events occur. Instead of polling for results, your server receives HTTP POST requests automatically.
Overview
Section titled “Overview”Webhooks notify you of:
- Batch validation completion
- Quota threshold warnings
- Monthly quota resets
- API key activity alerts
Configuration
Section titled “Configuration”Dashboard Setup
Section titled “Dashboard Setup”- Go to Settings > Webhooks in your dashboard
- Click Add Webhook Endpoint
- Enter your endpoint URL (must be HTTPS)
- Select events to subscribe to
- Copy the signing secret for verification
API Configuration
Section titled “API Configuration”curl -X POST https://api.spamidate.com/webhooks \ -H "X-API-Key: spm_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/spamidate", "events": ["batch.completed", "quota.warning"] }'Event Types
Section titled “Event Types”| Event | Description |
|---|---|
validation.completed | Single email validation finished |
batch.completed | Batch validation finished |
quota.warning | Usage reached 80% of limit |
quota.exceeded | Usage reached 100% (grace period) |
quota.reset | Monthly quota reset |
key.created | New API key generated |
key.disabled | API key was disabled |
Payload Structure
Section titled “Payload Structure”All webhook payloads follow this structure:
{ "id": "evt_abc123def456", "type": "batch.completed", "timestamp": "2024-01-15T10:30:00.000Z", "data": { // Event-specific data }}| Field | Type | Description |
|---|---|---|
id | string | Unique event ID |
type | string | Event type |
timestamp | string | ISO 8601 timestamp |
data | object | Event-specific payload |
Event Payloads
Section titled “Event Payloads”batch.completed
Section titled “batch.completed”Sent when a batch validation request finishes processing.
{ "id": "evt_abc123", "type": "batch.completed", "timestamp": "2024-01-15T10:30:00.000Z", "data": { "batchId": "batch_xyz789", "total": 100, "valid": 85, "invalid": 10, "warning": 5, "processingTime": 4523, "resultsUrl": "https://api.spamidate.com/batches/batch_xyz789/results" }}quota.warning
Section titled “quota.warning”Sent when usage reaches 80% of your monthly limit.
{ "id": "evt_def456", "type": "quota.warning", "timestamp": "2024-01-15T10:30:00.000Z", "data": { "limit": 10000, "used": 8000, "remaining": 2000, "percentage": 80, "daysRemaining": 16, "projectedUsage": 15000 }}quota.exceeded
Section titled “quota.exceeded”Sent when usage reaches 100% (entering grace period for Growth+ tiers).
{ "id": "evt_ghi789", "type": "quota.exceeded", "timestamp": "2024-01-15T10:30:00.000Z", "data": { "limit": 10000, "used": 10000, "graceLimit": 12000, "inGracePeriod": true, "resetsAt": "2024-02-01T00:00:00.000Z" }}quota.reset
Section titled “quota.reset”Sent at the start of a new billing period.
{ "id": "evt_jkl012", "type": "quota.reset", "timestamp": "2024-02-01T00:00:00.000Z", "data": { "previousUsage": 9500, "newLimit": 10000, "periodStart": "2024-02-01T00:00:00.000Z", "periodEnd": "2024-02-29T23:59:59.999Z" }}Signature Verification
Section titled “Signature Verification”All webhooks include a signature header for verification. Always verify signatures before processing.
Headers
Section titled “Headers”| Header | Description |
|---|---|
X-Spamidate-Signature | HMAC SHA-256 signature |
X-Spamidate-Timestamp | Request timestamp |
Verification Process
Section titled “Verification Process”- Get the raw request body
- Get the timestamp from header
- Concatenate:
timestamp.body - Compute HMAC SHA-256 with your signing secret
- Compare with provided signature
const crypto = require('crypto');
function verifyWebhook(payload, signature, timestamp, secret) { const signedPayload = `${timestamp}.${payload}`; const expectedSignature = crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex');
// Timing-safe comparison return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) );}
// Express middlewareapp.post('/webhooks/spamidate', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-spamidate-signature']; const timestamp = req.headers['x-spamidate-timestamp'];
if (!verifyWebhook(req.body, signature, timestamp, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); }
const event = JSON.parse(req.body); // Process event...
res.status(200).send('OK');});import hmacimport hashlib
def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool: signed_payload = f"{timestamp}.{payload.decode()}" expected = hmac.new( secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected)
# Flask example@app.route('/webhooks/spamidate', methods=['POST'])def webhook(): signature = request.headers.get('X-Spamidate-Signature') timestamp = request.headers.get('X-Spamidate-Timestamp')
if not verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET): return 'Invalid signature', 401
event = request.json # Process event...
return 'OK', 200import ( "crypto/hmac" "crypto/sha256" "encoding/hex")
func verifyWebhook(payload, signature, timestamp, secret string) bool { signedPayload := timestamp + "." + payload mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(signedPayload)) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expected))}Handling Webhooks
Section titled “Handling Webhooks”Respond Quickly
Section titled “Respond Quickly”Return a 2xx status within 30 seconds. Process events asynchronously.
app.post('/webhooks/spamidate', async (req, res) => { // Verify signature first if (!verifySignature(req)) { return res.status(401).send('Invalid signature'); }
// Acknowledge immediately res.status(200).send('OK');
// Process async const event = JSON.parse(req.body); await processEventAsync(event);});Idempotency
Section titled “Idempotency”Webhooks may be delivered more than once. Use the event ID for idempotency:
const processedEvents = new Set();
async function processEvent(event) { if (processedEvents.has(event.id)) { return; // Already processed }
// Process event...
processedEvents.add(event.id);}For production, store processed event IDs in your database:
CREATE TABLE processed_webhooks ( event_id TEXT PRIMARY KEY, processed_at TIMESTAMP DEFAULT NOW());Retry Policy
Section titled “Retry Policy”Failed webhook deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 24 hours |
After 6 failed attempts, the webhook is marked as failed. Check your dashboard for failed deliveries.
Testing Webhooks
Section titled “Testing Webhooks”Test Endpoint
Section titled “Test Endpoint”Send a test event from your dashboard or use the API:
curl -X POST https://api.spamidate.com/webhooks/test \ -H "X-API-Key: spm_live_..." \ -H "Content-Type: application/json" \ -d '{"type": "batch.completed"}'Local Development
Section titled “Local Development”Use a tunnel service for local testing:
# Using ngrokngrok http 3000
# Configure webhook URL# https://abc123.ngrok.io/webhooks/spamidateWebhook Logs
Section titled “Webhook Logs”View delivery logs in your dashboard:
- Payload sent
- Response received
- Status code
- Response time
- Retry history
Best Practices
Section titled “Best Practices”-
Always verify signatures - Never trust unverified payloads
-
Respond quickly - Return 2xx within 30 seconds, process async
-
Handle duplicates - Implement idempotency with event IDs
-
Log everything - Log received webhooks for debugging
-
Monitor failures - Set up alerts for failed webhook deliveries
-
Use HTTPS - Webhook URLs must use HTTPS
-
Handle all event types - Gracefully ignore unknown events
switch (event.type) {case 'batch.completed':handleBatchCompleted(event);break;default:console.log(`Unknown event type: ${event.type}`);}