Webhooks enable you to receive real-time notifications about NovaMed operations, including order status updates, shipment notifications, and medication request changes. This guide will help you set up and integrate webhooks into your system.
Webhooks are HTTP callbacks that allow NovaMed to notify your system when specific events occur. Instead of polling the API for updates, you register a webhook endpoint, and we send POST requests to your endpoint whenever events happen.
- ✅ Real-time updates - Receive notifications immediately when events occur
- ✅ Efficient - No need to poll the API repeatedly
- ✅ Reliable - Built-in retry mechanisms for failed deliveries
- ✅ Scalable - Handles high-volume event streams efficiently
| Environment | URL |
|---|---|
| Development | https://novamed-feapidev.nimbushealthcaretest.com |
| Production | https://feapi.novamed.care |
Register your webhook URL with NovaMed:
curl -X POST https://novamed-feapidev.nimbushealthcaretest.com/api/external/webhook \
-H "x-api-key: your-api-key-here" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"clinic_id": "550e8400-e29b-41d4-a716-446655440000",
"webhook_url": "https://your-domain.com/webhooks/novamed"
}'Response:
{
"success": true,
"data": {
"webhook_id": "660e8400-e29b-41d4-a716-446655440001",
"webhook_url": "https://your-domain.com/webhooks/novamed",
"clinic_id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2025-01-15T10:00:00.000Z"
},
"message": "Webhook registered successfully"
}Your webhook endpoint must:
- Accept HTTPS POST requests
- Return HTTP 200 OK quickly (process asynchronously)
- Verify the
x-api-keyheader - Handle duplicate events (idempotency)
NovaMed sends the following webhook events:
| Event | Description |
|---|---|
medication_order:verified | Medication order has been verified |
medication_order:submitted | Medication order has been submitted |
medication_order:acknowledged | Medication order has been acknowledged |
medication_order:filled | Medication order has been filled |
| Event | Description |
|---|---|
shipment:created | Shipment has been created (includes tracking info) |
shipment:cancelled | Shipment has been cancelled |
| Event | Description |
|---|---|
practitioner:activated | Practitioner account has been activated |
All webhook events follow this structure:
{
"event_name": "medication_order:verified",
"event_data": {
// Event-specific data
}
}{
"event_name": "medication_order:verified",
"event_data": {
"id": "a7570e3c-4338-485f-9465-ee09793c2d46",
"clinic_id": "2a7d8da1-2f69-46e1-87a4-04490ab73c41",
"patient_id": "8456ce26-05db-4fcd-bebd-495bd7bc04df",
"practitioner_id": "a7570e3c-4338-485f-9465-ee09793c2d46",
"rx_number": "123456"
}
}Note: The
rx_numberfield is not available for all events.
After a shipment is created, additional fields are included:
{
"event_name": "shipment:created",
"event_data": {
"id": "a7570e3c-4338-485f-9465-ee09793c2d46",
"clinic_id": "2a7d8da1-2f69-46e1-87a4-04490ab73c41",
"patient_id": "8456ce26-05db-4fcd-bebd-495bd7bc04df",
"practitioner_id": "a7570e3c-4338-485f-9465-ee09793c2d46",
"rx_number": "123456",
"shipping_label": "https://example.com/shipping-label.pdf",
"shipment_id": "8456ce26-05db-4fcd-bebd-495bd7bc04df",
"shipment_provider": "Ameriship",
"tracking_url": "https://example.com/tracking"
}
}{
"event_name": "practitioner:activated",
"event_data": {
"practitioner_id": "4f407bca-2a9b-4dde-8240-e53331a5a986",
"practitioner_name": "Dr. John Smith",
"practitioner_email": "dr.smith@clinic.com",
"practitioner_phone": "+1-555-0123",
"practitioner_npi": "1234567890",
"practitioner_status": "active"
}
}const express = require('express');
const app = express();
app.use(express.json());
// Webhook endpoint
app.post('/webhooks/novamed', async (req, res) => {
// 1. Verify API key
const apiKey = req.headers['x-api-key'];
if (apiKey !== process.env.NOVAMED_WEBHOOK_SECRET) {
return res.status(401).json({ error: 'Unauthorized' });
}
// 2. Return 200 immediately
res.status(200).json({ received: true });
// 3. Process event asynchronously
try {
await processWebhookEvent(req.body);
} catch (error) {
console.error('Error processing webhook:', error);
}
});
async function processWebhookEvent(payload) {
const { event_name, event_data } = payload;
// Check for duplicate events using medication order ID
if (event_data.id && await isEventProcessed(event_data.id, event_name)) {
console.log('Duplicate event, skipping:', event_name, event_data.id);
return;
}
switch (event_name) {
case 'medication_order:verified':
await handleMedicationOrderVerified(event_data);
break;
case 'medication_order:submitted':
await handleMedicationOrderSubmitted(event_data);
break;
case 'medication_order:acknowledged':
await handleMedicationOrderAcknowledged(event_data);
break;
case 'medication_order:filled':
await handleMedicationOrderFilled(event_data);
break;
case 'shipment:created':
await handleShipmentCreated(event_data);
break;
case 'shipment:cancelled':
await handleShipmentCancelled(event_data);
break;
case 'practitioner:activated':
await handlePractitionerActivated(event_data);
break;
default:
console.log('Unknown event:', event_name);
}
// Mark event as processed
if (event_data.id) {
await markEventProcessed(event_data.id, event_name);
}
}
async function handleMedicationOrderVerified(data) {
console.log('Medication order verified:', data.id);
// Update order status in your system
}
async function handleMedicationOrderSubmitted(data) {
console.log('Medication order submitted:', data.id);
// Track submission status
}
async function handleMedicationOrderAcknowledged(data) {
console.log('Medication order acknowledged:', data.id);
// Update acknowledgment status
}
async function handleMedicationOrderFilled(data) {
console.log('Medication order filled:', data.id);
// Process filled order
}
async function handleShipmentCreated(data) {
console.log('Shipment created:', data.shipment_id);
console.log('Tracking URL:', data.tracking_url);
// Send tracking information to patient
}
async function handleShipmentCancelled(data) {
console.log('Shipment cancelled:', data.shipment_id);
// Handle cancellation
}
async function handlePractitionerActivated(data) {
console.log('Practitioner activated:', data.practitioner_id);
console.log('Name:', data.practitioner_name);
// Update practitioner status in your system
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});from flask import Flask, request, jsonify
import os
import threading
app = Flask(__name__)
@app.route('/webhooks/novamed', methods=['POST'])
def webhook_handler():
# 1. Verify API key
api_key = request.headers.get('x-api-key')
if api_key != os.getenv('NOVAMED_WEBHOOK_SECRET'):
return jsonify({'error': 'Unauthorized'}), 401
# 2. Return 200 immediately
payload = request.get_json()
# 3. Process event asynchronously
thread = threading.Thread(target=process_webhook_event, args=(payload,))
thread.start()
return jsonify({'received': True}), 200
def process_webhook_event(payload):
event_name = payload.get('event_name')
event_data = payload.get('event_data')
handlers = {
'medication_order:verified': handle_medication_order_verified,
'medication_order:submitted': handle_medication_order_submitted,
'medication_order:acknowledged': handle_medication_order_acknowledged,
'medication_order:filled': handle_medication_order_filled,
'shipment:created': handle_shipment_created,
'shipment:cancelled': handle_shipment_cancelled,
'practitioner:activated': handle_practitioner_activated,
}
handler = handlers.get(event_name)
if handler:
handler(event_data)
else:
print(f'Unknown event: {event_name}')
def handle_medication_order_verified(data):
print(f"Medication order verified: {data.get('id')}")
# Update order status
def handle_medication_order_submitted(data):
print(f"Medication order submitted: {data.get('id')}")
# Track submission
def handle_medication_order_acknowledged(data):
print(f"Medication order acknowledged: {data.get('id')}")
# Update acknowledgment status
def handle_medication_order_filled(data):
print(f"Medication order filled: {data.get('id')}")
# Process filled order
def handle_shipment_created(data):
print(f"Shipment created: {data.get('shipment_id')}")
print(f"Tracking URL: {data.get('tracking_url')}")
# Send tracking info to patient
def handle_shipment_cancelled(data):
print(f"Shipment cancelled: {data.get('shipment_id')}")
# Handle cancellation
def handle_practitioner_activated(data):
print(f"Practitioner activated: {data.get('practitioner_id')}")
print(f"Name: {data.get('practitioner_name')}")
# Update practitioner status
if __name__ == '__main__':
app.run(port=3000)To remove a registered webhook:
curl -X DELETE https://novamed-feapidev.nimbushealthcaretest.com/api/external/webhook \
-H "x-api-key: your-api-key-here" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"clinic_id": "550e8400-e29b-41d4-a716-446655440000",
"webhook_id": "660e8400-e29b-41d4-a716-446655440001"
}'Response:
{
"success": true,
"message": "Webhook deleted successfully"
}Always verify the x-api-key header matches your webhook secret:
const apiKey = req.headers['x-api-key'];
if (apiKey !== process.env.NOVAMED_WEBHOOK_SECRET) {
return res.status(401).json({ error: 'Unauthorized' });
}Webhook endpoints must use HTTPS. NovaMed will not send webhooks to HTTP endpoints.
Handle duplicate events gracefully using the medication order id and event_name:
const processedEvents = new Map();
async function isEventProcessed(id, eventName) {
const key = `${id}:${eventName}`;
return processedEvents.has(key);
}
async function markEventProcessed(id, eventName) {
const key = `${id}:${eventName}`;
processedEvents.set(key, Date.now());
// Also persist to database for reliability
}Your endpoint should return HTTP 200 OK within 5 seconds. Process events asynchronously:
app.post('/webhooks/novamed', (req, res) => {
// Return immediately
res.status(200).json({ received: true });
// Process asynchronously
processWebhookEvent(req.body).catch(console.error);
});NovaMed will retry failed webhook deliveries:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
After 5 failed attempts, the webhook delivery is marked as failed.
If your endpoint returns a non-200 status code, the webhook will be retried. To prevent retries:
- Return 200 OK even if processing fails
- Log errors for later processing
- Process asynchronously to avoid timeouts
Start your webhook server:
node webhook-server.jsExpose with ngrok:
ngrok http 3000Register webhook with the ngrok URL:
curl -X POST https://novamed-feapidev.nimbushealthcaretest.com/api/external/webhook \ -H "x-api-key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "clinic_id": "your-clinic-uuid", "webhook_url": "https://abc123.ngrok.io/webhooks/novamed" }'Trigger a test event by creating an order in the sandbox environment
Monitor ngrok requests at
http://localhost:4040
- ✅ Check webhook URL is registered correctly
- ✅ Verify endpoint is accessible (not behind firewall)
- ✅ Ensure endpoint uses HTTPS
- ✅ Check API key is correct
- ✅ Implement idempotency checking using
idandevent_name - ✅ Store processed event keys in database
- ✅ Return 200 OK quickly (< 5 seconds)
- ✅ Process events asynchronously
- ✅ Optimize database queries
- ✅ Use HTTPS - Required for webhook endpoints
- ✅ Verify API Key - Always check
x-api-keyheader - ✅ Return 200 OK Quickly - Within 5 seconds
- ✅ Process Asynchronously - Don't block the response
- ✅ Implement Idempotency - Handle duplicate events using
idandevent_name - ✅ Log Everything - For debugging and monitoring
- ✅ Handle Errors Gracefully - Don't crash on errors
- Register your webhook endpoint
- Implement webhook handler
- Test with development environment
- Deploy to production
- API Support: api@nimbus-os.com
- Development Environment:
https://novamed-feapidev.nimbushealthcaretest.com - Production Environment:
https://feapi.novamed.care
For more information, see the API Reference.