Skip to content

Webhook & Event Reference ​

Overview ​

OpenCHS supports webhooks for real-time event notifications. External systems can subscribe to events and receive HTTP POST requests when specific actions occur in the system.

Webhook Configuration ​

Registering a Webhook ​

sql
-- Webhook configuration table
CREATE TABLE webhook_subscription (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL,
  url VARCHAR(500) NOT NULL,
  secret VARCHAR(255) NOT NULL,
  events JSON NOT NULL,
  is_active BOOLEAN DEFAULT TRUE,
  
  -- Retry configuration
  max_retries INT DEFAULT 3,
  retry_delay_seconds INT DEFAULT 60,
  
  -- Statistics
  total_deliveries INT DEFAULT 0,
  failed_deliveries INT DEFAULT 0,
  last_delivery_at TIMESTAMP NULL,
  last_success_at TIMESTAMP NULL,
  
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  
  INDEX idx_is_active (is_active)
);

Register via API (Future Implementation) ​

bash
curl -X POST https://your-domain.com/helpline/api/webhooks \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{
    "name": "External CRM Integration",
    "url": "https://your-system.com/webhooks/openchs",
    "secret": "your-webhook-secret",
    "events": [
      "case.created",
      "case.updated",
      "case.resolved"
    ]
  }'

Webhook Payload Structure ​

All webhook requests follow this format:

json
{
  "event": "case.created",
  "timestamp": "2025-09-26T14:30:00Z",
  "webhook_id": "wh_abc123",
  "delivery_id": "del_xyz789",
  "data": {
    // Event-specific data
  }
}

HTTP Headers ​

POST /your-webhook-endpoint HTTP/1.1
Host: your-system.com
Content-Type: application/json
X-OpenCHS-Event: case.created
X-OpenCHS-Delivery: del_xyz789
X-OpenCHS-Signature: sha256=abc123...
User-Agent: OpenCHS-Webhooks/1.0

Signature Verification ​

Verify webhook authenticity using HMAC SHA256:

python
import hmac
import hashlib

def verify_webhook_signature(payload, signature, secret):
    """Verify webhook signature"""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(
        f"sha256={expected_signature}",
        signature
    )

# Usage in webhook handler
@app.route('/webhooks/openchs', methods=['POST'])
def handle_openchs_webhook():
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-OpenCHS-Signature')
    
    if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
        return {'error': 'Invalid signature'}, 401
    
    data = request.get_json()
    process_webhook_event(data)
    
    return {'status': 'received'}, 200

Available Events ​

Case Events ​

case.created ​

Triggered when a new case is created.

Payload:

json
{
  "event": "case.created",
  "timestamp": "2025-09-26T14:30:00Z",
  "data": {
    "case": {
      "id": 123,
      "case_number": "CASE-2025-001234",
      "title": "Child safety concern",
      "description": "Reported concern about child welfare",
      "category": "abuse",
      "subcategory": "physical_abuse",
      "priority": "high",
      "status": "open",
      "reporter_phone": "+254700123456",
      "reporter_email": "reporter@example.com",
      "child_age": 12,
      "child_gender": "female",
      "assigned_user_id": null,
      "created_at": "2025-09-26T14:30:00Z",
      "created_by": 5
    },
    "creator": {
      "id": 5,
      "username": "operator_john",
      "role": "operator"
    }
  }
}

case.updated ​

Triggered when case details are modified.

Payload:

json
{
  "event": "case.updated",
  "timestamp": "2025-09-26T15:45:00Z",
  "data": {
    "case": {
      "id": 123,
      "case_number": "CASE-2025-001234",
      "status": "assigned",
      "assigned_user_id": 8
      // ... other fields
    },
    "changes": {
      "status": {
        "old": "open",
        "new": "assigned"
      },
      "assigned_user_id": {
        "old": null,
        "new": 8
      }
    },
    "updated_by": {
      "id": 7,
      "username": "supervisor_jane",
      "role": "supervisor"
    }
  }
}

case.assigned ​

Triggered when case is assigned to a user.

Payload:

json
{
  "event": "case.assigned",
  "timestamp": "2025-09-26T15:45:00Z",
  "data": {
    "case": {
      "id": 123,
      "case_number": "CASE-2025-001234",
      "title": "Child safety concern"
    },
    "assigned_to": {
      "id": 8,
      "username": "case_manager_maria",
      "role": "case_manager",
      "organization": "Nairobi Helpline"
    },
    "assigned_by": {
      "id": 7,
      "username": "supervisor_jane"
    }
  }
}

case.status_changed ​

Triggered when case status changes.

Payload:

json
{
  "event": "case.status_changed",
  "timestamp": "2025-09-27T10:15:00Z",
  "data": {
    "case": {
      "id": 123,
      "case_number": "CASE-2025-001234"
    },
    "status": {
      "old": "in_progress",
      "new": "resolved"
    },
    "resolution_notes": "Case successfully resolved. Family provided support services.",
    "changed_by": {
      "id": 8,
      "username": "case_manager_maria"
    }
  }
}

case.escalated ​

Triggered when case is escalated.

Payload:

json
{
  "event": "case.escalated",
  "timestamp": "2025-09-26T16:00:00Z",
  "data": {
    "case": {
      "id": 123,
      "case_number": "CASE-2025-001234",
      "priority": "critical"
    },
    "escalation_reason": "Immediate danger to child",
    "escalated_to": {
      "id": 7,
      "username": "supervisor_jane",
      "role": "supervisor"
    },
    "escalated_by": {
      "id": 8,
      "username": "case_manager_maria"
    }
  }
}

case.resolved ​

Triggered when case is marked as resolved.

Payload:

json
{
  "event": "case.resolved",
  "timestamp": "2025-09-27T10:15:00Z",
  "data": {
    "case": {
      "id": 123,
      "case_number": "CASE-2025-001234",
      "title": "Child safety concern",
      "status": "resolved",
      "resolution_time_hours": 19
    },
    "resolution": {
      "outcome": "services_provided",
      "notes": "Family provided counseling and support services",
      "follow_up_required": true,
      "follow_up_date": "2025-10-27"
    },
    "resolved_by": {
      "id": 8,
      "username": "case_manager_maria"
    }
  }
}

Communication Events ​

communication.received ​

Triggered when new communication is logged.

Payload:

json
{
  "event": "communication.received",
  "timestamp": "2025-09-26T14:25:00Z",
  "data": {
    "communication": {
      "id": 456,
      "case_id": 123,
      "contact_type": "call",
      "direction": "inbound",
      "contact_address": "+254700123456",
      "duration": 180,
      "call_status": "answered",
      "disposition": "case_created",
      "created_at": "2025-09-26T14:25:00Z"
    },
    "case": {
      "id": 123,
      "case_number": "CASE-2025-001234"
    }
  }
}

audio.processed ​

Triggered when audio processing completes.

Payload:

json
{
  "event": "audio.processed",
  "timestamp": "2025-09-26T14:26:30Z",
  "data": {
    "communication_id": 456,
    "case_id": 123,
    "processing_results": {
      "transcript": "Ninahitaji msaada kwa mtoto...",
      "translation": "I need help for a child...",
      "language_detected": "sw",
      "classification": {
        "category": "child_protection",
        "subcategory": "abuse",
        "risk_level": "high",
        "confidence": 0.94
      },
      "entities": {
        "PERSON": ["Maria"],
        "LOC": ["Nairobi", "Kibera"],
        "AGE": ["12 years old"]
      }
    },
    "processing_time_seconds": 23.4
  }
}

AI Events ​

ai.prediction_complete ​

Triggered when AI case prediction completes.

Payload:

json
{
  "event": "ai.prediction_complete",
  "timestamp": "2025-09-26T14:26:00Z",
  "data": {
    "case_id": 123,
    "prediction": {
      "risk_level": "high",
      "confidence": 0.94,
      "classification": "child_protection",
      "recommended_actions": [
        "immediate_intervention",
        "notify_supervisor",
        "contact_child_services"
      ],
      "entities_detected": {
        "PERSON": ["Maria"],
        "LOC": ["Nairobi"],
        "ORG": ["City Hospital"]
      }
    },
    "model_version": "v1.2.3"
  }
}

User Events ​

user.login ​

Triggered when user successfully logs in.

Payload:

json
{
  "event": "user.login",
  "timestamp": "2025-09-26T08:00:00Z",
  "data": {
    "user": {
      "id": 5,
      "username": "operator_john",
      "role": "operator",
      "organization": "Nairobi Helpline"
    },
    "session": {
      "id": "sess_abc123",
      "ip_address": "192.168.1.100",
      "user_agent": "Mozilla/5.0..."
    }
  }
}

Webhook Best Practices ​

Idempotency ​

Handle duplicate deliveries gracefully:

python
class WebhookHandler:
    def __init__(self):
        self.processed_deliveries = set()
    
    def handle_webhook(self, data):
        delivery_id = data['delivery_id']
        
        # Check if already processed
        if delivery_id in self.processed_deliveries:
            return {'status': 'already_processed'}, 200
        
        # Process event
        self.process_event(data)
        
        # Mark as processed
        self.processed_deliveries.add(delivery_id)
        
        return {'status': 'processed'}, 200

Async Processing ​

Process webhooks asynchronously to avoid timeouts:

python
from celery import Celery

app = Celery('webhooks')

@app.route('/webhooks/openchs', methods=['POST'])
def receive_webhook():
    data = request.get_json()
    
    # Queue for async processing
    process_webhook_async.delay(data)
    
    # Return immediately
    return {'status': 'queued'}, 202

@app.task
def process_webhook_async(data):
    """Process webhook in background"""
    event_type = data['event']
    
    if event_type == 'case.created':
        handle_case_created(data['data'])
    elif event_type == 'case.updated':
        handle_case_updated(data['data'])
    # ... handle other events

Error Handling ​

python
@app.route('/webhooks/openchs', methods=['POST'])
def handle_webhook():
    try:
        # Verify signature
        if not verify_signature(request):
            return {'error': 'Invalid signature'}, 401
        
        data = request.get_json()
        
        # Validate payload
        if not validate_webhook_payload(data):
            return {'error': 'Invalid payload'}, 400
        
        # Process event
        result = process_event(data)
        
        return {'status': 'success', 'result': result}, 200
        
    except Exception as e:
        # Log error
        logger.error(f"Webhook processing error: {str(e)}")
        
        # Return 500 to trigger retry
        return {'error': 'Processing failed'}, 500

Retry Logic ​

OpenCHS will retry failed webhook deliveries:

Attempt 1: Immediate
Attempt 2: After 60 seconds
Attempt 3: After 120 seconds
Attempt 4: After 240 seconds

Respond quickly to avoid retries:

  • Return 2xx status within 5 seconds
  • Queue long-running tasks
  • Use async processing

Testing Webhooks ​

bash
# Simulate webhook locally
curl -X POST http://localhost:5000/webhooks/openchs \
  -H "Content-Type: application/json" \
  -H "X-OpenCHS-Event: case.created" \
  -H "X-OpenCHS-Signature: sha256=..." \
  -d '{
    "event": "case.created",
    "timestamp": "2025-09-26T14:30:00Z",
    "data": {
      "case": {
        "id": 123,
        "case_number": "CASE-2025-001234"
      }
    }
  }'

Event Filtering ​

Subscribe to specific events only:

json
{
  "events": [
    "case.created",
    "case.resolved",
    "case.escalated"
  ]
}

Or use wildcards:

json
{
  "events": [
    "case.*",           // All case events
    "communication.*",  // All communication events
    "ai.prediction_complete"
  ]
}

Monitoring ​

Webhook Delivery Logs ​

sql
CREATE TABLE webhook_delivery_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  webhook_subscription_id INT NOT NULL,
  delivery_id VARCHAR(100) UNIQUE NOT NULL,
  event VARCHAR(100) NOT NULL,
  payload JSON NOT NULL,
  
  -- Delivery details
  url VARCHAR(500) NOT NULL,
  http_status INT,
  response_body TEXT,
  response_time_ms INT,
  
  -- Retry information
  attempt_number INT DEFAULT 1,
  max_attempts INT DEFAULT 3,
  next_retry_at TIMESTAMP NULL,
  
  -- Status
  status ENUM('pending', 'delivered', 'failed', 'exhausted') DEFAULT 'pending',
  
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  delivered_at TIMESTAMP NULL,
  
  FOREIGN KEY (webhook_subscription_id) REFERENCES webhook_subscription(id),
  INDEX idx_status (status),
  INDEX idx_created_at (created_at)
);

Query Delivery Status ​

sql
-- Recent deliveries
SELECT 
    ws.name,
    wdl.event,
    wdl.status,
    wdl.http_status,
    wdl.attempt_number,
    wdl.created_at
FROM 
    webhook_delivery_log wdl
    INNER JOIN webhook_subscription ws ON wdl.webhook_subscription_id = ws.id
WHERE 
    wdl.created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY 
    wdl.created_at DESC
LIMIT 100;

-- Failed deliveries requiring attention
SELECT 
    ws.name,
    ws.url,
    COUNT(*) as failed_count,
    MAX(wdl.created_at) as last_failure
FROM 
    webhook_delivery_log wdl
    INNER JOIN webhook_subscription ws ON wdl.webhook_subscription_id = ws.id
WHERE 
    wdl.status = 'failed'
    AND wdl.created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY 
    ws.id, ws.name, ws.url
HAVING 
    failed_count >= 5;

Troubleshooting ​

Webhook Not Received ​

  1. Check webhook is active and URL is correct
  2. Verify firewall allows OpenCHS IP addresses
  3. Check webhook delivery logs for errors
  4. Test endpoint manually with curl

Invalid Signature ​

  1. Verify webhook secret matches registration
  2. Check signature calculation implementation
  3. Ensure payload is not modified before verification

Slow Processing ​

  1. Implement async processing
  2. Return 202 Accepted immediately
  3. Process webhooks in background queue

For additional support, see the Integration Guide.