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.stackmod.info |
| Production | https://feapi.novamed.care |
Register your webhook URL with NovaMed:
curl -X POST https://novamed-feapidev.stackmod.info/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 |
|---|---|
order:created | New order has been created |
order:processing | Order is being processed |
order:shipped | Order has been shipped |
order:delivered | Order has been delivered |
order:cancelled | Order has been cancelled |
| Event | Description |
|---|---|
medication_request:created | New medication request created |
medication_request:approved | Medication request approved |
medication_request:filled | Medication has been filled |
medication_request:cancelled | Medication request cancelled |
| Event | Description |
|---|---|
refill:requested | Refill request submitted |
refill:approved | Refill request approved |
refill:denied | Refill request denied |
All webhook events follow this structure:
{
"event_type": "order:shipped",
"event_id": "evt_abc123xyz",
"timestamp": "2025-01-15T10:30:00.000Z",
"clinic_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
// Event-specific data
}
}{
"event_type": "order:shipped",
"event_id": "evt_abc123xyz",
"timestamp": "2025-01-15T10:30:00.000Z",
"clinic_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"order_id": "ord_12345",
"patient_id": "pat_67890",
"medication_request_id": "med_req_11111",
"tracking_number": "1Z999AA10123456784",
"carrier": "UPS",
"estimated_delivery": "2025-01-18",
"shipping_address": {
"line1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"zip": "94102"
}
}
}{
"event_type": "medication_request:approved",
"event_id": "evt_def456xyz",
"timestamp": "2025-01-15T09:00:00.000Z",
"clinic_id": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"medication_request_id": "med_req_11111",
"patient_id": "pat_67890",
"practitioner_id": "prac_22222",
"medication_name": "Testosterone Cypionate",
"quantity": 1,
"refills": 3,
"approved_at": "2025-01-15T09:00:00.000Z"
}
}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_type, data, event_id } = payload;
// Check for duplicate events
if (await isEventProcessed(event_id)) {
console.log('Duplicate event, skipping:', event_id);
return;
}
switch (event_type) {
case 'order:shipped':
await handleOrderShipped(data);
break;
case 'order:delivered':
await handleOrderDelivered(data);
break;
case 'medication_request:approved':
await handleMedicationApproved(data);
break;
case 'refill:approved':
await handleRefillApproved(data);
break;
default:
console.log('Unknown event:', event_type);
}
// Mark event as processed
await markEventProcessed(event_id);
}
async function handleOrderShipped(data) {
console.log('Order shipped:', data.order_id);
// Update your system with tracking information
// Send notification to patient
}
async function handleOrderDelivered(data) {
console.log('Order delivered:', data.order_id);
// Update order status in your database
}
async function handleMedicationApproved(data) {
console.log('Medication approved:', data.medication_request_id);
// Trigger next steps in your workflow
}
async function handleRefillApproved(data) {
console.log('Refill approved:', data.refill_id);
// Process refill order
}
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_type = payload.get('event_type')
data = payload.get('data')
event_id = payload.get('event_id')
handlers = {
'order:shipped': handle_order_shipped,
'order:delivered': handle_order_delivered,
'medication_request:approved': handle_medication_approved,
'refill:approved': handle_refill_approved,
}
handler = handlers.get(event_type)
if handler:
handler(data)
else:
print(f'Unknown event: {event_type}')
def handle_order_shipped(data):
print(f"Order shipped: {data.get('order_id')}")
# Update your system with tracking information
def handle_order_delivered(data):
print(f"Order delivered: {data.get('order_id')}")
# Update order status
def handle_medication_approved(data):
print(f"Medication approved: {data.get('medication_request_id')}")
# Trigger workflow
def handle_refill_approved(data):
print(f"Refill approved: {data.get('refill_id')}")
# Process refill
if __name__ == '__main__':
app.run(port=3000)To remove a registered webhook:
curl -X DELETE https://novamed-feapidev.stackmod.info/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 event_id:
const processedEvents = new Map();
async function isEventProcessed(eventId) {
return processedEvents.has(eventId);
}
async function markEventProcessed(eventId) {
processedEvents.set(eventId, 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.stackmod.info/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
event_id - ✅ Store processed event IDs 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
event_id - ✅ 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.stackmod.info - Production Environment:
https://feapi.novamed.care
For more information, see the API Reference.