Skip to main content

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

patient.created
event
Triggered when a new patient is registeredPayload includes: patient_id, name, contact info, branch
patient.updated
event
Triggered when patient information is modifiedPayload includes: patient_id, updated fields
patient.status_changed
event
Triggered when patient status changes (active/inactive/on_hold)Payload includes: patient_id, old_status, new_status

Appointment Events

appointment.scheduled
event
Triggered when new appointment is createdPayload includes: appointment_id, patient_id, doctor_id, date, time
appointment.confirmed
event
Triggered when appointment status changes to confirmedPayload includes: appointment_id, confirmation_time
appointment.completed
event
Triggered when appointment is marked as completedPayload includes: appointment_id, completion_time, services_rendered
appointment.cancelled
event
Triggered when appointment is cancelledPayload includes: appointment_id, cancellation_reason

Financial Events

payment.received
event
Triggered when payment is recordedPayload includes: payment_id, amount, method, patient_id, budget_id
budget.created
event
Triggered when new treatment budget is createdPayload includes: budget_id, patient_id, total_amount, items
budget.approved
event
Triggered when budget status changes to approvedPayload includes: budget_id, approval_time

Clinical Events

medical_record.created
event
Triggered when new medical record entry is addedPayload includes: record_id, patient_id, doctor_id, diagnosis
Triggered when patient signs informed consentPayload includes: consent_id, patient_id, procedure_name

System Events

reminder.sent
event
Triggered when appointment reminder is sentPayload includes: appointment_id, method, sent_time
inventory.low_stock
event
Triggered when inventory drops below minimum thresholdPayload 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 });
}

Webhook Payload Format

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

Build docs developers (and LLMs) love