Webhooks
Receive real-time notifications when events happen in your ReachScore account instead of polling the API.
Why use webhooks? Instead of repeatedly polling the API to check if a test has completed, webhooks push events to your server the moment they happen. This is more efficient and provides faster response times.
How Webhooks Work
Register an Endpoint
Create a webhook endpoint in your dashboard or via API, specifying the events you want to receive.
Receive Events
When an event occurs, we send a POST request to your endpoint with the event payload.
Acknowledge Receipt
Return a 2xx status code within 30 seconds to acknowledge receipt. We'll retry on failure.
Creating a Webhook Endpoint
Register a webhook endpoint to start receiving events:
curl -X POST https://api.reachscore.co/v1/webhooks/endpoints \
-H "Authorization: Bearer $REACHSCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/reachscore",
"events": ["test.completed", "test.failed", "domain.verified"],
"description": "Production webhook handler"
}'The response includes a secret which you must store securely for signature verification:
{
"id": "whe_7xK2mN9pQrT4v",
"url": "https://your-server.com/webhooks/reachscore",
"events": ["test.completed", "test.failed", "domain.verified"],
"secret": "whsec_abc123xyz...",
"status": "active",
"created_at": "2024-01-15T10:30:00Z"
}Available Events
| Event | Description |
|---|---|
| test.completed | A deliverability test finished successfully with results |
| test.failed | A test failed (email not received within timeout) |
| domain.verified | Domain ownership was successfully verified |
| domain.verification_failed | Domain verification check failed |
| monitor.alert | A scheduled monitor triggered an alert condition |
| alert.triggered | An alert rule condition was met |
| alert.resolved | A previously triggered alert has resolved |
Event Payload Format
All webhook payloads follow a consistent structure:
{
"id": "evt_8nL3pR2qSsU5w",
"object": "event",
"type": "test.completed",
"created_at": "2024-01-15T10:31:15Z",
"livemode": true,
"data": {
"id": "test_7xK2mN9pQrT4v",
"object": "test",
"status": "completed",
"score": 92,
"grade": "A",
"from_address": "notifications@yourcompany.com",
"auth_results": {
"spf": "pass",
"dkim": "pass",
"dmarc": "pass"
},
"inbox_placement": {
"gmail": "inbox",
"outlook": "inbox"
},
"completed_at": "2024-01-15T10:31:15Z"
}
}Verifying Webhook Signatures
Every webhook request includes a signature header that you should verify to ensure the request came from ReachScore. The signature is computed using HMAC-SHA256 with your endpoint's secret.
Request Headers
| Header | Description |
|---|---|
| X-ReachScore-Signature | HMAC-SHA256 signature of the payload |
| X-ReachScore-Timestamp | Unix timestamp when the webhook was sent |
| X-ReachScore-Event-ID | Unique identifier for the event |
Verification Example (Node.js)
import crypto from 'crypto';
function verifyWebhookSignature(payload, signature, timestamp, secret) {
// Check timestamp is within 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
throw new Error('Timestamp too old');
}
// Compute expected signature
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures using timing-safe comparison
const expected = Buffer.from(expectedSignature, 'hex');
const received = Buffer.from(signature.replace('sha256=', ''), 'hex');
if (!crypto.timingSafeEqual(expected, received)) {
throw new Error('Invalid signature');
}
return true;
}
// In your webhook handler
app.post('/webhooks/reachscore', (req, res) => {
try {
verifyWebhookSignature(
req.body,
req.headers['x-reachscore-signature'],
req.headers['x-reachscore-timestamp'],
process.env.WEBHOOK_SECRET
);
// Process the event
const event = req.body;
switch (event.type) {
case 'test.completed':
handleTestCompleted(event.data);
break;
// ... handle other events
}
res.status(200).send('OK');
} catch (error) {
res.status(400).send('Invalid signature');
}
});Verification Example (Python)
import hmac
import hashlib
import time
import json
def verify_webhook_signature(payload, signature, timestamp, secret):
# Check timestamp is within 5 minutes
now = int(time.time())
if abs(now - int(timestamp)) > 300:
raise ValueError("Timestamp too old")
# Compute expected signature
signed_payload = f"{timestamp}.{json.dumps(payload)}"
expected_signature = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Compare signatures
received = signature.replace("sha256=", "")
if not hmac.compare_digest(expected_signature, received):
raise ValueError("Invalid signature")
return TrueRetry Behavior
If your endpoint fails to respond with a 2xx status code within 30 seconds, we retry with exponential backoff:
- Attempt 1: Immediate
- Attempt 2: After 1 minute
- Attempt 3: After 5 minutes
- Attempt 4: After 30 minutes
- Attempt 5: After 2 hours
- Attempt 6: After 8 hours
- 30 second timeout per attempt
- 6 total attempts over ~10 hours
- Events marked as failed after all retries
- View failed deliveries in dashboard
Best Practices
Return quickly
Process webhooks asynchronously. Return a 200 immediately and queue the work for background processing.
Handle duplicates
Use the event ID for idempotency. The same event may be delivered more than once during retries.
Always verify signatures
Never process webhook payloads without verifying the signature first.
Use HTTPS
Webhook endpoints must use HTTPS. We do not send webhooks to HTTP URLs.
Testing Webhooks Locally
Use a service like ngrok to expose your local development server to receive webhooks. Create a tunnel with ngrok http 3000 and use the generated URL as your webhook endpoint.