Webhooks
Webhooks provide a way to extend MedBackend's request validation and processing with your own custom logic. You can use webhooks to validate data, enforce business rules, or trigger external workflows.
Webhooks are executed at two points in the request lifecycle:
- Pre-request hook: Executed after validation but before the request is sent to the FHIR server. Can allow or block the request.
- Post-response hook: Executed after the FHIR server responds. Can modify the response or trigger downstream actions.
Webhook Configuration
Webhooks are configured per resource and operation within your validation rules. Add pre_request_hook or post_response_hook to any validation rule:
{
"client_role": "Patient",
"entity_name": "Patient",
"operation": "create",
"validator": "patient_compartment",
"pre_request_hook": {
"url": "https://your-service.com/webhooks/validate-patient",
"enabled": true
}
}
You can configure both hooks on the same rule:
{
"client_role": "Practitioner",
"entity_name": "Observation",
"operation": "create",
"validator": "legitimate_interest",
"pre_request_hook": {
"url": "https://your-service.com/webhooks/validate-observation",
"enabled": true
},
"post_response_hook": {
"url": "https://your-service.com/webhooks/observation-created",
"enabled": true
}
}
Full Configuration Example
Here's a complete RBAC configuration with webhooks:
{
"rbac": {
"default_access": "Forbidden",
"validators": {
"patient_compartment": { "enabled": true },
"legitimate_interest": { "enabled": true }
},
"validation_rules": [
{
"client_role": "Patient",
"entity_name": "Patient",
"operation": "read",
"validator": "patient_compartment"
},
{
"client_role": "Patient",
"entity_name": "Patient",
"operation": "update",
"validator": "patient_compartment",
"pre_request_hook": {
"url": "https://your-service.com/webhooks/validate-patient-update",
"enabled": true
}
},
{
"client_role": "Practitioner",
"entity_name": "Observation",
"operation": "create",
"validator": "legitimate_interest",
"post_response_hook": {
"url": "https://your-service.com/webhooks/observation-created",
"enabled": true
}
}
],
"webhook_settings": {
"enabled": true,
"timeout_ms": 30000,
"retry_attempts": 3
}
}
}
Webhook Payload
Your webhook endpoint receives a JSON payload with the following structure:
Pre-Request Hook Payload
{
"hook_type": "pre_request",
"event_type": "Patient.create",
"timestamp": "2024-03-15T10:30:00Z",
"request_id": "req-abc123",
"project_id": "your-project-id",
"client_request": {
"operation": "create",
"entity_name": "Patient",
"entity_id": null,
"body": {
"resourceType": "Patient",
"name": [{ "family": "Smith", "given": ["John"] }],
"birthDate": "1990-01-15"
}
},
"context": {
"client_role": "Practitioner",
"client_entity_id": "practitioner-123",
"user_id": "user-456"
}
}
Post-Response Hook Payload
{
"hook_type": "post_response",
"event_type": "Patient.create",
"timestamp": "2024-03-15T10:30:01Z",
"request_id": "req-abc123",
"project_id": "your-project-id",
"client_request": {
"operation": "create",
"entity_name": "Patient",
"entity_id": null
},
"backend_response": {
"status": 201,
"body": {
"resourceType": "Patient",
"id": "patient-789",
"meta": {
"versionId": "1",
"lastUpdated": "2024-03-15T10:30:00Z"
},
"name": [{ "family": "Smith", "given": ["John"] }],
"birthDate": "1990-01-15"
}
},
"context": {
"client_role": "Practitioner",
"client_entity_id": "practitioner-123",
"user_id": "user-456"
}
}
Webhook Response
Your webhook endpoint should return a JSON response indicating whether to proceed.
Allow the Request
{
"proceed": true
}
Block the Request
{
"proceed": false,
"error_message": "Patient identifier already exists in external system."
}
When proceed is false, MedBackend will stop processing and return the error message to the client.
Modify the Response (Post-Response Hook Only)
The post-response hook can modify what gets returned to the client:
{
"proceed": true,
"backend_response": {
"resourceType": "Patient",
"id": "patient-789",
"name": [{ "family": "Smith", "given": ["John"] }]
}
}
Webhook Settings
Configure global webhook behavior in the webhook_settings section:
| Setting | Default | Description |
|---|---|---|
enabled | false | Master switch to enable/disable all webhooks |
timeout_ms | 30000 | Request timeout in milliseconds |
retry_attempts | 3 | Number of retry attempts for failed deliveries |
Security
When an HMAC secret is configured, MedBackend signs all webhook requests. The signature is included in the X-Webhook-Signature header:
X-Webhook-Signature: sha256=<hex-encoded-signature>
Verifying Signatures
import hmac
import hashlib
import json
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
# Re-serialize with sorted keys (as MedBackend does)
payload_dict = json.loads(payload)
canonical = json.dumps(payload_dict, sort_keys=True).encode('utf-8')
expected = hmac.new(secret.encode(), canonical, hashlib.sha256).hexdigest()
actual = signature.replace("sha256=", "")
return hmac.compare_digest(expected, actual)
Example: Pre-Request Validation
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhooks/validate-patient', methods=['POST'])
def validate_patient():
event = request.json
patient = event['client_request']['body']
# Require at least one identifier
if not patient.get('identifier'):
return {
'proceed': False,
'error_message': 'Patient must have at least one identifier'
}
# Validate MRN format
for id in patient.get('identifier', []):
if id.get('system') == 'urn:mrn' and not id.get('value', '').startswith('MRN-'):
return {
'proceed': False,
'error_message': 'MRN must start with "MRN-"'
}
return {'proceed': True}
Example: Post-Response Audit Logging
@app.route('/webhooks/observation-created', methods=['POST'])
def log_observation():
event = request.json
observation = event['backend_response']['body']
# Log to audit system
audit_log.create({
'event': 'observation_created',
'resource_id': observation['id'],
'practitioner': event['context']['client_entity_id'],
'timestamp': event['timestamp']
})
return {'proceed': True}