Overview
While OdontologyApp doesn’t currently have a built-in webhook system, the application architecture provides several extension points where webhook functionality can be integrated. This guide documents potential webhook use cases and how to implement webhook notifications for key events.
Webhook Use Cases
Webhooks enable real-time event notifications to external systems, allowing you to:
External Analytics Send appointment and payment events to analytics platforms
Marketing Automation Trigger email campaigns when patients complete treatments
SMS Services Send automated SMS reminders via third-party providers
Accounting Systems Sync payment data with QuickBooks or other accounting software
Backup Systems Replicate critical data to external backup services
Custom Integrations Connect to custom internal systems or dashboards
Event Types
The following events in OdontologyApp are ideal candidates for webhook notifications:
Patient Events
Triggered when a new patient is registered Payload includes: patient_id, name, contact info, branch
Triggered when patient information is modified Payload includes: patient_id, updated fields
Triggered when patient status changes (active/inactive/on_hold) Payload includes: patient_id, old_status, new_status
Appointment Events
Triggered when new appointment is created Payload includes: appointment_id, patient_id, doctor_id, date, time
Triggered when appointment status changes to confirmed Payload includes: appointment_id, confirmation_time
Triggered when appointment is marked as completed Payload includes: appointment_id, completion_time, services_rendered
Triggered when appointment is cancelled Payload includes: appointment_id, cancellation_reason
Financial Events
Triggered when payment is recorded Payload includes: payment_id, amount, method, patient_id, budget_id
Triggered when new treatment budget is created Payload includes: budget_id, patient_id, total_amount, items
Triggered when budget status changes to approved Payload includes: budget_id, approval_time
Clinical Events
Triggered when new medical record entry is added Payload includes: record_id, patient_id, doctor_id, diagnosis
Triggered when patient signs informed consent Payload includes: consent_id, patient_id, procedure_name
System Events
Triggered when appointment reminder is sent Payload includes: appointment_id, method, sent_time
Triggered when inventory drops below minimum threshold Payload includes: item_id, current_stock, min_stock
Implementation Guide
Database Schema
To implement webhooks, first create the necessary tables:
-- Webhook configurations
CREATE TABLE webhooks (
id INT AUTO_INCREMENT PRIMARY KEY ,
name VARCHAR ( 100 ) NOT NULL ,
url VARCHAR ( 500 ) NOT NULL ,
events JSON NOT NULL ,
secret VARCHAR ( 100 ),
is_active TINYINT ( 1 ) DEFAULT 1 ,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Webhook delivery logs
CREATE TABLE webhook_deliveries (
id INT AUTO_INCREMENT PRIMARY KEY ,
webhook_id INT NOT NULL ,
event_type VARCHAR ( 50 ) NOT NULL ,
payload JSON NOT NULL ,
response_status INT ,
response_body TEXT ,
delivered_at TIMESTAMP NULL ,
failed_at TIMESTAMP NULL ,
retry_count INT DEFAULT 0 ,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON DELETE CASCADE
);
-- Example webhook configuration
INSERT INTO webhooks ( name , url , events, secret , is_active)
VALUES (
'Payment Notifications' ,
'https://api.example.com/webhooks/payments' ,
'["payment.received", "payment.refunded"]' ,
'whsec_abc123xyz' ,
1
);
Webhook Service Module
Create a reusable webhook service:
// src/lib/server/webhooks.js
import { pool } from './db' ;
import crypto from 'crypto' ;
export async function triggerWebhook ( eventType , payload ) {
try {
// Find all active webhooks listening to this event
const [ webhooks ] = await pool . query (
`SELECT * FROM webhooks
WHERE is_active = 1
AND JSON_CONTAINS(events, ?)
` ,
[ JSON . stringify ( eventType )]
);
// Send to each webhook
for ( const webhook of webhooks ) {
await deliverWebhook ( webhook , eventType , payload );
}
} catch ( error ) {
console . error ( 'Webhook trigger error:' , error );
}
}
async function deliverWebhook ( webhook , eventType , payload ) {
const deliveryPayload = {
event: eventType ,
timestamp: new Date (). toISOString (),
data: payload
};
// Generate signature for verification
const signature = generateSignature (
JSON . stringify ( deliveryPayload ),
webhook . secret
);
try {
const response = await fetch ( webhook . url , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'X-Webhook-Signature' : signature ,
'X-Webhook-Event' : eventType ,
'User-Agent' : 'OdontologyApp-Webhooks/1.0'
},
body: JSON . stringify ( deliveryPayload )
});
// Log successful delivery
await pool . query (
`INSERT INTO webhook_deliveries
(webhook_id, event_type, payload, response_status, response_body, delivered_at)
VALUES (?, ?, ?, ?, ?, NOW())
` ,
[
webhook . id ,
eventType ,
JSON . stringify ( deliveryPayload ),
response . status ,
await response . text ()
]
);
} catch ( error ) {
// Log failed delivery
await pool . query (
`INSERT INTO webhook_deliveries
(webhook_id, event_type, payload, failed_at)
VALUES (?, ?, ?, NOW())
` ,
[ webhook . id , eventType , JSON . stringify ( deliveryPayload )]
);
console . error ( `Webhook delivery failed for ${ webhook . name } :` , error );
}
}
function generateSignature ( payload , secret ) {
return crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
}
export function verifyWebhookSignature ( payload , signature , secret ) {
const expectedSignature = generateSignature ( payload , secret );
return crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
);
}
Integration Points
Add webhook triggers to existing API endpoints:
Payment Event Example
// src/routes/api/finance/payments/+server.js
import { pool } from '$lib/server/db' ;
import { triggerWebhook } from '$lib/server/webhooks' ;
import { json } from '@sveltejs/kit' ;
export async function POST ({ request , locals }) {
const user = locals . user ;
if ( ! user ) return json ({ message: 'No autorizado' }, { status: 401 });
const { patient_id , budget_id , amount , payment_method , notes } = await request . json ();
try {
// Register payment
const [ result ] = await pool . query (
'CALL sp_register_payment(?, ?, ?, ?, ?)' ,
[ patient_id , budget_id , amount , payment_method , notes ]
);
const paymentId = result [ 0 ][ 0 ]. payment_id ;
// Trigger webhook
await triggerWebhook ( 'payment.received' , {
payment_id: paymentId ,
patient_id ,
budget_id ,
amount ,
payment_method ,
notes ,
processed_by: user . id ,
timestamp: new Date (). toISOString ()
});
return json ({ success: true , payment_id: paymentId });
} catch ( err ) {
return json ({ message: 'Error al registrar pago' }, { status: 500 });
}
}
Appointment Event Example
// src/routes/api/appointments/+server.js
import { triggerWebhook } from '$lib/server/webhooks' ;
export async function POST ({ request , locals }) {
// ... appointment creation logic ...
const appointmentId = result . insertId ;
// Trigger webhook after successful creation
await triggerWebhook ( 'appointment.scheduled' , {
appointment_id: appointmentId ,
patient_id ,
doctor_id ,
branch_id ,
appointment_date ,
appointment_time ,
duration_minutes ,
created_by: user . id ,
timestamp: new Date (). toISOString ()
});
return json ({ success: true , id: appointmentId });
}
export async function PATCH ({ request , locals }) {
const { id , status } = await request . json ();
// ... status update logic ...
// Trigger different events based on status
if ( status === 'confirmed' ) {
await triggerWebhook ( 'appointment.confirmed' , {
appointment_id: id ,
confirmation_time: new Date (). toISOString (),
confirmed_by: user . id
});
} else if ( status === 'completed' ) {
await triggerWebhook ( 'appointment.completed' , {
appointment_id: id ,
completion_time: new Date (). toISOString (),
completed_by: user . id
});
} else if ( status === 'cancelled' ) {
await triggerWebhook ( 'appointment.cancelled' , {
appointment_id: id ,
cancellation_time: new Date (). toISOString (),
cancelled_by: user . id
});
}
return json ({ success: true });
}
Webhook Management API
Create API endpoints for managing webhook configurations:
// src/routes/api/webhooks/+server.js
import { pool } from '$lib/server/db' ;
import { json } from '@sveltejs/kit' ;
import crypto from 'crypto' ;
// List all webhooks
export async function GET ({ locals }) {
const user = locals . user ;
if ( user ?. role !== 'admin' ) {
return json ({ message: 'No autorizado' }, { status: 403 });
}
const [ webhooks ] = await pool . query (
'SELECT id, name, url, events, is_active, created_at FROM webhooks'
);
return json ({ webhooks });
}
// Create new webhook
export async function POST ({ request , locals }) {
const user = locals . user ;
if ( user ?. role !== 'admin' ) {
return json ({ message: 'No autorizado' }, { status: 403 });
}
const { name , url , events } = await request . json ();
// Generate webhook secret
const secret = 'whsec_' + crypto . randomBytes ( 32 ). toString ( 'hex' );
const [ result ] = await pool . query (
'INSERT INTO webhooks (name, url, events, secret) VALUES (?, ?, ?, ?)' ,
[ name , url , JSON . stringify ( events ), secret ]
);
return json ({
success: true ,
id: result . insertId ,
secret // Return secret only once during creation
});
}
// Delete webhook
export async function DELETE ({ request , locals }) {
const user = locals . user ;
if ( user ?. role !== 'admin' ) {
return json ({ message: 'No autorizado' }, { status: 403 });
}
const { id } = await request . json ();
await pool . query ( 'DELETE FROM webhooks WHERE id = ?' , [ id ]);
return json ({ success: true });
}
All webhooks follow this standard format:
{
"event" : "payment.received" ,
"timestamp" : "2026-03-07T15:30:00.000Z" ,
"data" : {
"payment_id" : 123 ,
"patient_id" : 45 ,
"budget_id" : 67 ,
"amount" : 500.00 ,
"payment_method" : "credit_card" ,
"notes" : "Initial payment" ,
"processed_by" : 1 ,
"timestamp" : "2026-03-07T15:30:00.000Z"
}
}
Webhook Security
Signature Verification
All webhook deliveries include an X-Webhook-Signature header for verification:
// Receiving webhook on external service
import crypto from 'crypto' ;
export async function POST ( request ) {
const signature = request . headers . get ( 'X-Webhook-Signature' );
const payload = await request . text ();
const secret = process . env . WEBHOOK_SECRET ;
// Verify signature
const expectedSignature = crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
if ( signature !== expectedSignature ) {
return new Response ( 'Invalid signature' , { status: 401 });
}
// Process webhook
const data = JSON . parse ( payload );
console . log ( 'Received event:' , data . event , data . data );
return new Response ( 'OK' , { status: 200 });
}
Best Practices
Never trust webhook data without verifying the HMAC signature. This prevents malicious actors from sending fake events.
Only accept webhook URLs that use HTTPS to ensure encrypted transmission of sensitive patient data.
Webhook deliveries can fail due to network issues. Implement exponential backoff retry logic for failed deliveries.
Keep detailed logs of all webhook deliveries, including failures, for debugging and audit purposes.
Retry Logic
Implement automatic retries for failed webhook deliveries:
// Background job for retrying failed webhooks
export async function retryFailedWebhooks () {
const [ failed ] = await pool . query ( `
SELECT * FROM webhook_deliveries
WHERE failed_at IS NOT NULL
AND delivered_at IS NULL
AND retry_count < 3
AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY created_at ASC
LIMIT 10
` );
for ( const delivery of failed ) {
const [ webhook ] = await pool . query (
'SELECT * FROM webhooks WHERE id = ?' ,
[ delivery . webhook_id ]
);
if ( webhook [ 0 ]?. is_active ) {
await deliverWebhook (
webhook [ 0 ],
delivery . event_type ,
JSON . parse ( delivery . payload ). data
);
// Increment retry count
await pool . query (
'UPDATE webhook_deliveries SET retry_count = retry_count + 1 WHERE id = ?' ,
[ delivery . id ]
);
}
}
}
Testing Webhooks
Local Testing with RequestBin
For local development, use RequestBin or similar services:
# Create a test webhook
curl -X POST http://localhost:5173/api/webhooks \
-H "Content-Type: application/json" \
-d '{
"name": "Test Webhook",
"url": "https://requestbin.com/r/your-bin-id",
"events": ["payment.received", "appointment.scheduled"]
}'
Mock Webhook Receiver
// test/webhook-receiver.js
import express from 'express' ;
const app = express ();
app . post ( '/webhook' , express . json (), ( req , res ) => {
console . log ( 'Received webhook:' );
console . log ( 'Event:' , req . headers [ 'x-webhook-event' ]);
console . log ( 'Signature:' , req . headers [ 'x-webhook-signature' ]);
console . log ( 'Payload:' , JSON . stringify ( req . body , null , 2 ));
res . sendStatus ( 200 );
});
app . listen ( 3000 , () => {
console . log ( 'Mock webhook receiver listening on port 3000' );
});
Notifications Internal notification system for staff alerts
Reminders Appointment reminder system with WhatsApp and email