Skip to main content

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:

SettingDefaultDescription
enabledfalseMaster switch to enable/disable all webhooks
timeout_ms30000Request timeout in milliseconds
retry_attempts3Number 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}