Skip to content

Webhooks

Business+

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.

Webhooks notify you of:

  • Batch validation completion
  • Quota threshold warnings
  • Monthly quota resets
  • API key activity alerts

  1. Go to Settings > Webhooks in your dashboard
  2. Click Add Webhook Endpoint
  3. Enter your endpoint URL (must be HTTPS)
  4. Select events to subscribe to
  5. Copy the signing secret for verification
Terminal window
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"]
}'

EventDescription
validation.completedSingle email validation finished
batch.completedBatch validation finished
quota.warningUsage reached 80% of limit
quota.exceededUsage reached 100% (grace period)
quota.resetMonthly quota reset
key.createdNew API key generated
key.disabledAPI key was disabled

All webhook payloads follow this structure:

{
"id": "evt_abc123def456",
"type": "batch.completed",
"timestamp": "2024-01-15T10:30:00.000Z",
"data": {
// Event-specific data
}
}
FieldTypeDescription
idstringUnique event ID
typestringEvent type
timestampstringISO 8601 timestamp
dataobjectEvent-specific payload

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"
}
}

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
}
}

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"
}
}

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"
}
}

All webhooks include a signature header for verification. Always verify signatures before processing.

HeaderDescription
X-Spamidate-SignatureHMAC SHA-256 signature
X-Spamidate-TimestampRequest timestamp
  1. Get the raw request body
  2. Get the timestamp from header
  3. Concatenate: timestamp.body
  4. Compute HMAC SHA-256 with your signing secret
  5. 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 middleware
app.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');
});

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);
});

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()
);

Failed webhook deliveries are retried with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
624 hours

After 6 failed attempts, the webhook is marked as failed. Check your dashboard for failed deliveries.


Send a test event from your dashboard or use the API:

Terminal window
curl -X POST https://api.spamidate.com/webhooks/test \
-H "X-API-Key: spm_live_..." \
-H "Content-Type: application/json" \
-d '{"type": "batch.completed"}'

Use a tunnel service for local testing:

Terminal window
# Using ngrok
ngrok http 3000
# Configure webhook URL
# https://abc123.ngrok.io/webhooks/spamidate

View delivery logs in your dashboard:

  • Payload sent
  • Response received
  • Status code
  • Response time
  • Retry history

  1. Always verify signatures - Never trust unverified payloads

  2. Respond quickly - Return 2xx within 30 seconds, process async

  3. Handle duplicates - Implement idempotency with event IDs

  4. Log everything - Log received webhooks for debugging

  5. Monitor failures - Set up alerts for failed webhook deliveries

  6. Use HTTPS - Webhook URLs must use HTTPS

  7. 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}`);
    }