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
-- 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)
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:
{
"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.0Signature Verification
Verify webhook authenticity using HMAC SHA256:
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'}, 200Available Events
Case Events
case.created
Triggered when a new case is created.
Payload:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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:
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'}, 200Async Processing
Process webhooks asynchronously to avoid timeouts:
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 eventsError Handling
@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'}, 500Retry Logic
OpenCHS will retry failed webhook deliveries:
Attempt 1: Immediate
Attempt 2: After 60 seconds
Attempt 3: After 120 seconds
Attempt 4: After 240 secondsRespond quickly to avoid retries:
- Return 2xx status within 5 seconds
- Queue long-running tasks
- Use async processing
Testing Webhooks
# 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:
{
"events": [
"case.created",
"case.resolved",
"case.escalated"
]
}Or use wildcards:
{
"events": [
"case.*", // All case events
"communication.*", // All communication events
"ai.prediction_complete"
]
}Monitoring
Webhook Delivery Logs
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
-- 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
- Check webhook is active and URL is correct
- Verify firewall allows OpenCHS IP addresses
- Check webhook delivery logs for errors
- Test endpoint manually with curl
Invalid Signature
- Verify webhook secret matches registration
- Check signature calculation implementation
- Ensure payload is not modified before verification
Slow Processing
- Implement async processing
- Return 202 Accepted immediately
- Process webhooks in background queue
For additional support, see the Integration Guide.