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:

Table of Contents

  1. How Webhooks Work
  2. Setting Up Webhooks
  3. Event Types & Payloads
  4. Security & Verification
  5. Best Practices
  6. Troubleshooting
  7. Testing & Development

Business Events Catalog

DomainEventTypical ConsumerBusiness Outcome
Policy / Auditpolicy.updated, policy.audit_scheduled, audit.status.changedCarrier policy admin, MGA platformsTrigger endorsements, schedule physical audits, update broker status
Payroll / Exposureemployee.created, payroll.submission.received, class_code.changedPayroll reporters, exposure enginesKeep classified wages current for PayGo and audit reconciliation
Compliancedocument.uploaded, document.requested, audit.closedAudit teams, regulators, carrier complianceMaintain audit trail, auto-generate DOI packets, chase missing evidence
Financialinvoice.issued, payment.posted, premium.deltaBilling teams, ERP connectorsSync 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:

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

Triggered 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

Triggered 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

Triggered 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

Triggered 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

Triggered 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

Triggered 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

Triggered 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

Triggered 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

Triggered 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

Triggered 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/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

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

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 events

PHP

<?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 3000

Use 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/audit1

2. 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]