Webhooks Guide
Learn how to receive real-time event notifications via webhooks
Webhooks Integration Guide
Overview
Webhooks enable real-time communication between Audit1 and your systems. When important events occur in Audit1, we'll send HTTP POST requests to your specified endpoints, allowing you to react immediately to changes in policies, employees, payments, and audits.
Important: Webhooks are configured through your Audit1 portal dashboard (Webhooks section), not programmatically via API. Use webhooks to RECEIVE event notifications FROM Audit1.
Portal URLs by User Type:
- Employers: https://employer.audit1.info
- Payroll Companies: https://payrollcompany.audit1.info
- Carriers: https://carrier.audit1.info
Table of Contents
- How Webhooks Work
- Setting Up Webhooks
- Event Types & Payloads
- Security & Verification
- Best Practices
- Troubleshooting
- Testing & Development
Business Events Catalog
| Domain | Event | Typical Consumer | Business Outcome |
|---|---|---|---|
| Policy / Audit | policy.updated, policy.audit_scheduled, audit.status.changed | Carrier policy admin, MGA platforms | Trigger endorsements, schedule physical audits, update broker status |
| Payroll / Exposure | employee.created, payroll.submission.received, class_code.changed | Payroll reporters, exposure engines | Keep classified wages current for PayGo and audit reconciliation |
| Compliance | document.uploaded, document.requested, audit.closed | Audit teams, regulators, carrier compliance | Maintain audit trail, auto-generate DOI packets, chase missing evidence |
| Financial | invoice.issued, payment.posted, premium.delta | Billing teams, ERP connectors | Sync receivables, reconcile cash events, adjust earned premium |
Event Prioritization Framework
- Regulatory – events that satisfy compliance deadlines (audit close, document required).
- Revenue – anything that affects earned premium or collections (invoice, payment, endorsement).
- Experience – signals that keep employers and brokers informed (request, reminder, status change).
Operational Readiness Checklist
- Define escalation owners per domain (policy vs. payroll) for webhook failures.
- Map each webhook to a downstream SLA (e.g., payroll updates must apply within 5 minutes).
- Document replay strategy for each consumer so retried deliveries remain idempotent.
- Capture
audit1-event-idandaudit1-delivery-idin your logs for forensic audits.
Growth Playbook
- Use webhooks to enrich broker or carrier portals with real-time audit health indicators.
- Combine
audit.closedevents with API data pulls to auto-generate premium adjustment memos. - Feed
employee.updatedevents into risk models to surface higher-touch audit prospects.
How Webhooks Work
The Webhook Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Audit1 │ │ Event │ │ Your Server │
│ System │───▶│ Triggers │───▶│ Endpoint │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Webhook │ │ Process │
│ Payload │ │ Event │
└─────────────┘ └─────────────┘
Process:
- Event Occurs: Something happens in Audit1 (policy created, employee updated, etc.)
- Event Processing: Our system processes the event and prepares webhook payload
- HTTP POST: We send POST request to your configured webhook URL
- Your Response: Your endpoint processes the event and responds with 2xx status
- Confirmation: We mark the webhook delivery as successful
Delivery Guarantees
- At-least-once delivery: Events may be delivered multiple times
- Retry mechanism: Failed deliveries are retried with exponential backoff
- 24-hour retention: We attempt delivery for 24 hours before giving up
- Idempotency: Each event has a unique ID for deduplication
Setting Up Webhooks
Step 1: Create Webhook Endpoint
Your webhook endpoint must:
- Accept POST requests
- Return 2xx status code for successful processing
- Respond within 30 seconds
- Handle duplicate events (idempotency)
Example Express.js Endpoint:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/audit1', (req, res) => {
const signature = req.headers['audit1-signature'];
const payload = req.body;
// Verify webhook signature (see Security section)
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(400).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
try {
// Process the event
processWebhookEvent(event);
// Always respond with 2xx
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing error:', error);
// Return 5xx for retries, 4xx to stop retries
res.status(500).json({ error: 'Processing failed' });
}
});Step 2: Register Webhook
Endpoint: POST /api/v1/webhooks
Request:
{
"owner_id": "emp_1234567890",
"owner_type": "employer",
"url": "https://your-domain.com/webhooks/audit1",
"events": [
"policy.created",
"policy.updated",
"employee.created",
"employee.updated",
"payment.processed"
]
}Response:
{
"webhook": {
"_id": "507f1f77bcf86cd799439012",
"id": "wh_1234567890",
"url": "https://your-domain.com/webhooks/audit1",
"events": ["policy.created", "policy.updated", "employee.created", "employee.updated", "payment.processed"],
"owner_id": "emp_1234567890",
"owner_type": "employer",
"is_active": true,
"created_at": "2024-01-15T10:35:00.000Z",
"updated_at": "2024-01-15T10:35:00.000Z"
},
"secret": "whsec_1234567890abcdef1234567890abcdef1234567890abcdef12"
}⚠️ Important: Store the webhook secret securely! You'll need it to verify webhook signatures.
Step 3: Test Your Webhook
Manual Test:
curl -X POST https://your-domain.com/webhooks/audit1 \
-H "Content-Type: application/json" \
-H "Audit1-Signature: t=1642291200,v1=test_signature" \
-d '{
"id": "evt_test_123",
"type": "policy.created",
"created_at": "2024-01-15T10:40:00.000Z",
"data": {
"policy": {
"id": "pol_test_123",
"employer_id": "emp_1234567890"
}
}
}'Event Types & Payloads
Policy Events
policy.created
policy.createdTriggered when a new insurance policy is created.
{
"id": "evt_1234567890",
"type": "policy.created",
"created_at": "2024-01-15T10:40:00.000Z",
"data": {
"policy": {
"id": "pol_1234567890",
"employer_id": "emp_1234567890",
"policy_number": "POL-2024-001",
"effective_date": "2024-02-01T00:00:00.000Z",
"expiration_date": "2025-02-01T00:00:00.000Z",
"premium": 1500.00,
"currency": "USD",
"status": "active",
"coverage": {
"workers_comp": true,
"general_liability": true,
"employment_practices": false
}
}
},
"metadata": {
"source": "audit1_admin",
"user_id": "usr_9876543210",
"environment": "production"
}
}policy.updated
policy.updatedTriggered when policy details are modified.
{
"id": "evt_1234567891",
"type": "policy.updated",
"created_at": "2024-01-15T15:20:00.000Z",
"data": {
"policy": {
"id": "pol_1234567890",
"employer_id": "emp_1234567890",
"policy_number": "POL-2024-001",
"premium": 1750.00, // Updated
"status": "active"
},
"changes": {
"premium": {
"old_value": 1500.00,
"new_value": 1750.00
}
}
}
}policy.cancelled
policy.cancelledTriggered when a policy is cancelled.
{
"id": "evt_1234567892",
"type": "policy.cancelled",
"created_at": "2024-01-15T16:30:00.000Z",
"data": {
"policy": {
"id": "pol_1234567890",
"employer_id": "emp_1234567890",
"status": "cancelled",
"cancellation_date": "2024-01-15T16:30:00.000Z",
"cancellation_reason": "Non-payment"
}
}
}Employee Events
employee.created
employee.createdTriggered when a new employee is added.
{
"id": "evt_2234567890",
"type": "employee.created",
"created_at": "2024-01-15T11:00:00.000Z",
"data": {
"employee": {
"id": "emp_2234567890",
"employer_id": "emp_1234567890",
"employee_number": "EMP-001",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"hire_date": "2024-01-15T00:00:00.000Z",
"department": "Engineering",
"job_title": "Software Developer",
"salary": 75000,
"status": "active"
}
}
}employee.updated
employee.updatedTriggered when employee information changes.
{
"id": "evt_2234567891",
"type": "employee.updated",
"created_at": "2024-01-15T14:15:00.000Z",
"data": {
"employee": {
"id": "emp_2234567890",
"employer_id": "emp_1234567890",
"salary": 80000, // Updated
"job_title": "Senior Software Developer" // Updated
},
"changes": {
"salary": {
"old_value": 75000,
"new_value": 80000
},
"job_title": {
"old_value": "Software Developer",
"new_value": "Senior Software Developer"
}
}
}
}employee.terminated
employee.terminatedTriggered when an employee is terminated.
{
"id": "evt_2234567892",
"type": "employee.terminated",
"created_at": "2024-01-15T17:00:00.000Z",
"data": {
"employee": {
"id": "emp_2234567890",
"employer_id": "emp_1234567890",
"status": "terminated",
"termination_date": "2024-01-15T17:00:00.000Z",
"termination_reason": "Resignation"
}
}
}Payment Events
payment.processed
payment.processedTriggered when a payment is successfully processed.
{
"id": "evt_3234567890",
"type": "payment.processed",
"created_at": "2024-01-15T12:00:00.000Z",
"data": {
"payment": {
"id": "pay_3234567890",
"employer_id": "emp_1234567890",
"policy_id": "pol_1234567890",
"amount": 1500.00,
"currency": "USD",
"payment_method": "ach",
"payment_date": "2024-01-15T12:00:00.000Z",
"invoice_id": "inv_4234567890",
"status": "completed"
}
}
}payment.failed
payment.failedTriggered when a payment fails to process.
{
"id": "evt_3234567891",
"type": "payment.failed",
"created_at": "2024-01-15T12:05:00.000Z",
"data": {
"payment": {
"id": "pay_3234567891",
"employer_id": "emp_1234567890",
"amount": 1500.00,
"status": "failed",
"failure_reason": "insufficient_funds",
"retry_at": "2024-01-16T12:00:00.000Z"
}
}
}Audit Events
audit.started
audit.startedTriggered when a new audit process begins.
{
"id": "evt_4234567890",
"type": "audit.started",
"created_at": "2024-01-15T13:00:00.000Z",
"data": {
"audit": {
"id": "aud_4234567890",
"employer_id": "emp_1234567890",
"audit_type": "workers_compensation",
"period_start": "2023-01-01T00:00:00.000Z",
"period_end": "2023-12-31T23:59:59.000Z",
"status": "in_progress",
"auditor_id": "aud_5234567890"
}
}
}audit.completed
audit.completedTriggered when an audit is completed.
{
"id": "evt_4234567891",
"type": "audit.completed",
"created_at": "2024-01-15T18:00:00.000Z",
"data": {
"audit": {
"id": "aud_4234567890",
"employer_id": "emp_1234567890",
"status": "completed",
"completion_date": "2024-01-15T18:00:00.000Z",
"findings": {
"additional_premium": 2500.00,
"return_premium": 0.00,
"total_adjustment": 2500.00
}
}
}
}Security & Verification
Webhook Signatures
Every webhook request from Audit1 includes HMAC-SHA256 signatures to verify authenticity and prevent tampering:
Headers Included:
X-Webhook-Signature: a1b2c3d4e5f6... (HMAC-SHA256 hex digest)
X-Webhook-Timestamp: 1704538800000 (Unix timestamp in milliseconds)
Content-Type: application/jsonWhy Verify Signatures?
✅ Authenticity: Confirm the webhook came from Audit1
✅ Integrity: Detect if payload was modified in transit
✅ Replay Protection: Timestamp prevents replay attacks
✅ Security: Protect against malicious actors
Verification Process
Step 1: Get Your Webhook Secret
When you create a webhook in the portal, you receive a secret:
whsec_abc123def456...
⚠️ Store this securely - treat it like an API key!
Step 2: Extract Headers
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body; // Raw JSON stringStep 3: Generate Expected Signature
const crypto = require('crypto');
function generateWebhookSignature(webhookSecret, timestamp, payload) {
// Build signature payload: timestamp + payload
const signedPayload = `${timestamp}.${payload}`;
// Calculate HMAC-SHA256
const hmac = crypto.createHmac('sha256', webhookSecret);
hmac.update(signedPayload);
return hmac.digest('hex');
}Step 4: Compare Signatures (Timing-Safe)
function verifyWebhookSignature(webhookSecret, providedSignature, timestamp, payload) {
try {
// Prevent replay attacks (5 minute window)
const currentTime = Date.now();
const timeDiff = Math.abs(currentTime - parseInt(timestamp));
if (timeDiff > 5 * 60 * 1000) {
console.error('Timestamp expired');
return false;
}
// Generate expected signature
const expectedSignature = generateWebhookSignature(webhookSecret, timestamp, payload);
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
} catch (error) {
console.error('Signature verification failed:', error);
return false;
}
}Complete Implementation Examples
JavaScript/Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
// IMPORTANT: Use raw body for signature verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhooks/audit1', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const webhookSecret = process.env.AUDIT1_WEBHOOK_SECRET;
// Verify signature
const isValid = verifyWebhookSignature(
webhookSecret,
signature,
timestamp,
req.rawBody
);
if (!isValid) {
console.error('❌ Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Process event
const event = req.body;
console.log('✅ Webhook verified:', event.type);
try {
processEvent(event);
res.status(200).json({ received: true });
} catch (error) {
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
function verifyWebhookSignature(webhookSecret, providedSignature, timestamp, payload) {
try {
const currentTime = Date.now();
const timeDiff = Math.abs(currentTime - parseInt(timestamp));
if (timeDiff > 5 * 60 * 1000) {
return false;
}
const signedPayload = `${timestamp}.${payload}`;
const hmac = crypto.createHmac('sha256', webhookSecret);
hmac.update(signedPayload);
const expectedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
} catch (error) {
return false;
}
}
function processEvent(event) {
switch (event.type) {
case 'policy.created':
handlePolicyCreated(event.data);
break;
case 'employee.updated':
handleEmployeeUpdated(event.data);
break;
// ... handle other events
}
}Python (Flask)
import hmac
import hashlib
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/audit1', methods=['POST'])
def webhook_handler():
signature = request.headers.get('X-Webhook-Signature')
timestamp = request.headers.get('X-Webhook-Timestamp')
webhook_secret = os.environ['AUDIT1_WEBHOOK_SECRET']
# Get raw body
payload = request.get_data(as_text=True)
# Verify signature
if not verify_webhook_signature(webhook_secret, signature, timestamp, payload):
print('❌ Invalid webhook signature')
return jsonify({'error': 'Invalid signature'}), 401
# Process event
event = request.json
print(f'✅ Webhook verified: {event["type"]}')
try:
process_event(event)
return jsonify({'received': True}), 200
except Exception as e:
print(f'Error processing webhook: {e}')
return jsonify({'error': 'Processing failed'}), 500
def verify_webhook_signature(webhook_secret, provided_signature, timestamp, payload):
try:
# Check timestamp (5 minute window)
current_time = int(time.time() * 1000)
time_diff = abs(current_time - int(timestamp))
if time_diff > 5 * 60 * 1000:
return False
# Generate expected signature
signed_payload = f"{timestamp}.{payload}"
expected_signature = hmac.new(
webhook_secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected_signature, provided_signature)
except Exception:
return False
def process_event(event):
event_type = event['type']
if event_type == 'policy.created':
handle_policy_created(event['data'])
elif event_type == 'employee.updated':
handle_employee_updated(event['data'])
# ... handle other eventsPHP
<?php
function verifyWebhookSignature($webhookSecret, $providedSignature, $timestamp, $payload) {
// Check timestamp (5 minute window)
$currentTime = round(microtime(true) * 1000);
$timeDiff = abs($currentTime - intval($timestamp));
if ($timeDiff > 5 * 60 * 1000) {
return false;
}
// Generate expected signature
$signedPayload = "$timestamp.$payload";
$expectedSignature = hash_hmac('sha256', $signedPayload, $webhookSecret);
// Constant-time comparison
return hash_equals($expectedSignature, $providedSignature);
}
// Webhook endpoint
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$webhookSecret = getenv('AUDIT1_WEBHOOK_SECRET');
// Get raw body
$payload = file_get_contents('php://input');
if (!verifyWebhookSignature($webhookSecret, $signature, $timestamp, $payload)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process event
$event = json_decode($payload, true);
processEvent($event);
http_response_code(200);
echo json_encode(['received' => true]);
?>Testing Signature Verification
Generate Test Signature
const crypto = require('crypto');
const webhookSecret = 'whsec_your_test_secret';
const timestamp = Date.now();
const payload = JSON.stringify({
type: 'policy.created',
data: { id: 'pol_123' }
});
const signedPayload = `${timestamp}.${payload}`;
const signature = crypto
.createHmac('sha256', webhookSecret)
.update(signedPayload)
.digest('hex');
console.log('Timestamp:', timestamp);
console.log('Signature:', signature);
// Test request
const testRequest = {
headers: {
'X-Webhook-Signature': signature,
'X-Webhook-Timestamp': timestamp.toString()
},
body: payload
};Curl Test
WEBHOOK_SECRET="whsec_your_test_secret"
TIMESTAMP=$(date +%s000)
PAYLOAD='{"type":"policy.created","data":{"id":"pol_123"}}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" | cut -d ' ' -f2)
curl -X POST http://localhost:3000/webhooks/audit1 \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: ${SIGNATURE}" \
-H "X-Webhook-Timestamp: ${TIMESTAMP}" \
-d "${PAYLOAD}"Additional Security Measures
1. HTTPS Only
// Enforce HTTPS in production
if (process.env.NODE_ENV === 'production' && req.protocol !== 'https') {
return res.status(400).json({ error: 'HTTPS required' });
}2. IP Allowlisting
const AUDIT1_IPS = [
'192.168.1.100',
'192.168.1.101',
'10.0.0.50'
];
function checkSourceIP(req, res, next) {
const clientIP = req.ip || req.connection.remoteAddress;
if (!AUDIT1_IPS.includes(clientIP)) {
return res.status(403).json({ error: 'Unauthorized IP' });
}
next();
}3. Rate Limiting
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many webhook requests'
});
app.use('/webhooks', webhookLimiter);Best Practices
1. Idempotency
Always handle duplicate events:
const processedEvents = new Set();
function processEvent(event) {
// Check if event already processed
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}
try {
// Process the event
handleEventType(event);
// Mark as processed
processedEvents.add(event.id);
// Optionally persist to database
await db.collection('processed_events').insertOne({
event_id: event.id,
processed_at: new Date()
});
} catch (error) {
console.error(`Error processing event ${event.id}:`, error);
throw error;
}
}2. Asynchronous Processing
Don't block the webhook response:
app.post('/webhooks/audit1', async (req, res) => {
try {
const event = verifyWebhook(req, process.env.WEBHOOK_SECRET);
// Respond immediately
res.status(200).json({ received: true });
// Process asynchronously
setImmediate(() => {
processEventAsync(event).catch(error => {
console.error('Async processing failed:', error);
// Add to retry queue if needed
});
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
async function processEventAsync(event) {
// Heavy processing logic here
switch (event.type) {
case 'policy.created':
await createInternalPolicy(event.data.policy);
await sendNotificationEmail(event.data.policy);
await updateDashboard(event.data.policy);
break;
// ... other event types
}
}3. Error Handling & Retries
function processEvent(event) {
const maxRetries = 3;
let attempt = 0;
async function attempt() {
try {
await handleEvent(event);
} catch (error) {
attempt++;
if (attempt <= maxRetries && isRetryableError(error)) {
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
setTimeout(attempt, delay);
} else {
// Send to dead letter queue or alert
await handleFailedEvent(event, error);
}
}
}
attempt();
}
function isRetryableError(error) {
// Retry on temporary failures, not on validation errors
return error.code === 'NETWORK_ERROR' ||
error.code === 'DATABASE_TIMEOUT' ||
error.status >= 500;
}4. Monitoring & Logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'webhooks.log' }),
new winston.transports.Console()
]
});
app.post('/webhooks/audit1', async (req, res) => {
const startTime = Date.now();
let eventType = 'unknown';
let eventId = 'unknown';
try {
const event = verifyWebhook(req, process.env.WEBHOOK_SECRET);
eventType = event.type;
eventId = event.id;
logger.info('Webhook received', {
eventId,
eventType,
timestamp: new Date().toISOString()
});
await processEvent(event);
logger.info('Webhook processed successfully', {
eventId,
eventType,
processingTime: Date.now() - startTime
});
res.status(200).json({ received: true });
} catch (error) {
logger.error('Webhook processing failed', {
eventId,
eventType,
error: error.message,
stack: error.stack,
processingTime: Date.now() - startTime
});
res.status(500).json({ error: 'Processing failed' });
}
});5. Database Considerations
Store webhook events for audit trail:
// MongoDB example
async function storeWebhookEvent(event, status) {
await db.collection('webhook_events').insertOne({
event_id: event.id,
event_type: event.type,
event_data: event.data,
received_at: new Date(),
status: status, // 'received', 'processed', 'failed'
processing_attempts: 0
});
}
// PostgreSQL example
async function storeWebhookEvent(event, status) {
await pool.query(`
INSERT INTO webhook_events (
event_id, event_type, event_data,
received_at, status, processing_attempts
) VALUES ($1, $2, $3, $4, $5, $6)
`, [
event.id, event.type, JSON.stringify(event.data),
new Date(), status, 0
]);
}Testing & Development
1. Local Development with ngrok
Setup ngrok:
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js
# In another terminal, expose your local server
ngrok http 3000Use ngrok URL for webhook:
const webhookUrl = 'https://abc123.ngrok.io/webhooks/audit1';
// Register webhook with ngrok URL
const webhook = await createWebhook({
owner_id: 'test_emp_123',
owner_type: 'employer',
url: webhookUrl,
events: ['policy.created']
});2. Webhook Testing Tool
Create a simple webhook tester:
// webhook-tester.js
const express = require('express');
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/test-webhook', (req, res) => {
console.log('Headers:', req.headers);
console.log('Body:', req.body.toString());
res.status(200).json({ status: 'received' });
});
app.listen(3001, () => {
console.log('Webhook tester running on port 3001');
});3. Webhook Simulator
Create test events for development:
// webhook-simulator.js
const crypto = require('crypto');
const fetch = require('node-fetch');
function createTestEvent(type, data) {
return {
id: `evt_test_${Date.now()}`,
type: type,
created_at: new Date().toISOString(),
data: data,
metadata: {
source: 'test_simulator',
environment: 'sandbox'
}
};
}
function signPayload(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const message = `${timestamp}.${payload}`;
const signature = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
return `t=${timestamp},v1=${signature}`;
}
async function sendTestWebhook(url, event, secret) {
const payload = JSON.stringify(event);
const signature = signPayload(payload, secret);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Audit1-Signature': signature
},
body: payload
});
console.log(`Webhook sent: ${response.status}`);
return response;
}
// Usage
const testEvent = createTestEvent('policy.created', {
policy: {
id: 'pol_test_123',
employer_id: 'emp_test_123',
premium: 1000.00
}
});
sendTestWebhook(
'http://localhost:3000/webhooks/audit1',
testEvent,
'your_webhook_secret'
);4. Unit Testing
// webhook.test.js
const request = require('supertest');
const app = require('./app');
describe('Webhook Endpoint', () => {
test('should accept valid webhook', async () => {
const payload = JSON.stringify({
id: 'evt_test_123',
type: 'policy.created',
data: { policy: { id: 'pol_123' } }
});
const signature = createSignature(payload, process.env.WEBHOOK_SECRET);
const response = await request(app)
.post('/webhooks/audit1')
.set('Content-Type', 'application/json')
.set('Audit1-Signature', signature)
.send(payload);
expect(response.status).toBe(200);
});
test('should reject invalid signature', async () => {
const payload = JSON.stringify({
id: 'evt_test_123',
type: 'policy.created'
});
const response = await request(app)
.post('/webhooks/audit1')
.set('Content-Type', 'application/json')
.set('Audit1-Signature', 'invalid_signature')
.send(payload);
expect(response.status).toBe(400);
});
});Troubleshooting
Common Issues
1. Webhook Not Receiving Events
Check:
- Webhook URL is accessible from internet
- Endpoint returns 2xx status code
- Firewall/security groups allow inbound traffic
- SSL certificate is valid (for HTTPS)
Debug:
# Test endpoint accessibility
curl -X POST https://your-domain.com/webhooks/audit1 \
-H "Content-Type: application/json" \
-d '{"test": true}'
# Check SSL certificate
curl -I https://your-domain.com/webhooks/audit12. Signature Verification Failing
Common causes:
- Wrong webhook secret
- Body modification by middleware
- Incorrect timestamp parsing
- Character encoding issues
Debug signature verification:
function debugSignature(req) {
const signature = req.headers['audit1-signature'];
const payload = req.body;
console.log('Received signature:', signature);
console.log('Payload length:', payload.length);
console.log('Payload type:', typeof payload);
const { timestamp, v1 } = parseSignature(signature);
const expected = createExpectedSignature(timestamp, payload, secret);
console.log('Parsed timestamp:', timestamp);
console.log('Received signature:', v1);
console.log('Expected signature:', expected);
console.log('Match:', v1 === expected);
}3. Timeouts and Retries
Monitor webhook performance:
const webhookMetrics = {
received: 0,
processed: 0,
failed: 0,
avgProcessingTime: 0
};
app.post('/webhooks/audit1', async (req, res) => {
const startTime = Date.now();
webhookMetrics.received++;
try {
const event = verifyWebhook(req, secret);
await processEvent(event);
webhookMetrics.processed++;
const processingTime = Date.now() - startTime;
webhookMetrics.avgProcessingTime =
(webhookMetrics.avgProcessingTime + processingTime) / 2;
res.status(200).json({ received: true });
} catch (error) {
webhookMetrics.failed++;
res.status(500).json({ error: 'Processing failed' });
}
});
// Expose metrics endpoint
app.get('/webhook-metrics', (req, res) => {
res.json(webhookMetrics);
});Debug Webhook Deliveries
Check delivery status:
// Get webhook delivery logs (if available via API)
async function getWebhookDeliveries(webhookId) {
const response = await fetch(
`/api/v1/webhooks/${webhookId}/deliveries`,
{
headers: { 'Authorization': `Bearer ${apiKey}` }
}
);
const deliveries = await response.json();
deliveries.forEach(delivery => {
console.log(`Delivery ${delivery.id}:`);
console.log(` Status: ${delivery.status}`);
console.log(` Attempts: ${delivery.attempts}`);
console.log(` Last attempt: ${delivery.last_attempt_at}`);
if (delivery.error) {
console.log(` Error: ${delivery.error}`);
}
});
}Webhook Management
List Webhooks
Endpoint: GET /api/v1/webhooks?owner_id={id}&owner_type={type}
async function listWebhooks(ownerId, ownerType) {
const response = await fetch(
`/api/v1/webhooks?owner_id=${ownerId}&owner_type=${ownerType}`,
{
headers: { 'Authorization': `Bearer ${apiKey}` }
}
);
const { webhooks } = await response.json();
return webhooks;
}Update Webhook Events
async function updateWebhookEvents(webhookId, events) {
const response = await fetch(`/api/v1/webhooks/${webhookId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({ events })
});
return response.json();
}Delete Webhook
Endpoint: DELETE /api/v1/webhooks/{id}?owner_id={owner_id}
async function deleteWebhook(webhookId, ownerId) {
const response = await fetch(
`/api/v1/webhooks/${webhookId}?owner_id=${ownerId}`,
{
method: 'DELETE',
headers: { 'Authorization': `Bearer ${apiKey}` }
}
);
return response.ok;
}For additional webhook support, contact [email protected]
Updated 1 day ago
