# 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](https://employer.audit1.info) > * Payroll Companies: [https://payrollcompany.audit1.info](https://payrollcompany.audit1.info) > * Carriers: [https://carrier.audit1.info](https://carrier.audit1.info) ## Table of Contents 1. [How Webhooks Work](#how-webhooks-work) 2. [Setting Up Webhooks](#setting-up-webhooks) 3. [Event Types & Payloads](#event-types--payloads) 4. [Security & Verification](#security--verification) 5. [Best Practices](#best-practices) 6. [Troubleshooting](#troubleshooting) 7. [Testing & Development](#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 1. **Regulatory** – events that satisfy compliance deadlines (audit close, document required). 2. **Revenue** – anything that affects earned premium or collections (invoice, payment, endorsement). 3. **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-id` and `audit1-delivery-id` in your logs for forensic audits. ### Growth Playbook * Use webhooks to enrich broker or carrier portals with real-time audit health indicators. * Combine `audit.closed` events with API data pulls to auto-generate premium adjustment memos. * Feed `employee.updated` events 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**: 1. **Event Occurs**: Something happens in Audit1 (policy created, employee updated, etc.) 2. **Event Processing**: Our system processes the event and prepares webhook payload 3. **HTTP POST**: We send POST request to your configured webhook URL 4. **Your Response**: Your endpoint processes the event and responds with 2xx status 5. **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**: ```javascript 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**: ```json { "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**: ```json { "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**: ```bash 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` Triggered when a new insurance policy is created. ```json { "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` Triggered when policy details are modified. ```json { "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` Triggered when a policy is cancelled. ```json { "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` Triggered when a new employee is added. ```json { "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": "john.doe@company.com", "hire_date": "2024-01-15T00:00:00.000Z", "department": "Engineering", "job_title": "Software Developer", "salary": 75000, "status": "active" } } } ``` #### `employee.updated` Triggered when employee information changes. ```json { "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` Triggered when an employee is terminated. ```json { "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` Triggered when a payment is successfully processed. ```json { "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` Triggered when a payment fails to process. ```json { "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` Triggered when a new audit process begins. ```json { "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` Triggered when an audit is completed. ```json { "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**: ```http X-Webhook-Signature: a1b2c3d4e5f6... (HMAC-SHA256 hex digest) X-Webhook-Timestamp: 1704538800000 (Unix timestamp in milliseconds) Content-Type: application/json ``` ### Why 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 ```javascript const signature = req.headers['x-webhook-signature']; const timestamp = req.headers['x-webhook-timestamp']; const payload = req.body; // Raw JSON string ``` #### Step 3: Generate Expected Signature ```javascript 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) ```javascript 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) ```javascript 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) ```python 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 events ``` #### PHP ```php 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 ```javascript 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 ```bash 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 ```javascript // Enforce HTTPS in production if (process.env.NODE_ENV === 'production' && req.protocol !== 'https') { return res.status(400).json({ error: 'HTTPS required' }); } ``` #### 2. IP Allowlisting ```javascript 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 ```javascript 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: ```javascript 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: ```javascript 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 ```javascript 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 ```javascript 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: ```javascript // 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**: ```bash # Install ngrok npm install -g ngrok # Start your local server node server.js # In another terminal, expose your local server ngrok http 3000 ``` **Use ngrok URL for webhook**: ```javascript 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: ```javascript // 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: ```javascript // 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 ```javascript // 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**: ```bash # 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/audit1 ``` #### 2. Signature Verification Failing **Common causes**: * Wrong webhook secret * Body modification by middleware * Incorrect timestamp parsing * Character encoding issues **Debug signature verification**: ```javascript 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**: ```javascript 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**: ```javascript // 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}` ```javascript 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 ```javascript 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}` ```javascript 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 developer-support@audit1.com*