Webhooks

Webhooks

Webhooks allow you to receive real-time notifications when events occur in Jump EHR. Instead of polling the API for changes, webhooks push data to your server as events happen.

Overview

When you configure a webhook endpoint, Jump EHR will send HTTP POST requests to your URL whenever subscribed events occur. This enables real-time integrations without consuming your API rate limit.

Event Types

Appointment Events

EventDescription
patient.createdA new patient record has been created
patient.updatedPatient demographics have changed
patient.archivedA patient has been archived

Patient webhook payloads include core demographics only. Use GET /patients/{id} to fetch the full record after receiving the notification.

Appointment Events

EventDescription
appointment.createdA new appointment has been booked
appointment.updatedAppointment details have changed
appointment.cancelledAn appointment has been cancelled
appointment.rescheduledAppointment time or date changed
appointment.completedAppointment status changed to completed

Questionnaire Response Events

EventDescription
questionnaire_response.createdA questionnaire response has been submitted
questionnaire_response.updatedA questionnaire response has been updated

Questionnaire response webhook payloads do not include the responses field (clinical data protection). Use GET /questionnaire-responses/{id} to fetch the full response after receiving the notification.

Payload Structure

All webhook payloads follow a consistent structure:

{
  "id": "evt_abc123",
  "type": "appointment.created",
  "created_at": "2025-01-15T10:30:00Z",
  "organization_id": "org_xyz789",
  "data": {
    "object": {
      "id": "apt_def456",
      "patient_id": "pat_ghi789",
      "clinician_profile_id": "cli_jkl012",
      "appointment_type_id": "type_mno345",
      "title": "Follow-up Consultation",
      "start_time": "2025-01-20T14:00:00Z",
      "end_time": "2025-01-20T14:30:00Z",
      "status": "confirmed",
      "location_id": "loc_pqr678",
      "is_remote": false,
      "attendee_name": "Sarah Johnson",
      "attendee_email": "sarah@example.com",
      "created_at": "2025-01-15T10:30:00Z",
      "updated_at": "2025-01-15T10:30:00Z"
    },
    "previous": null
  }
}

Questionnaire Response Payload Example

{
  "id": "evt_xyz789",
  "type": "questionnaire_response.created",
  "created_at": "2025-01-15T11:00:00Z",
  "organization_id": "org_xyz789",
  "data": {
    "object": {
      "id": "qr_abc123",
      "questionnaire_id": "qt_def456",
      "questionnaire_title": "New Patient Intake Form",
      "patient_id": "pat_ghi789",
      "appointment_id": null,
      "episode_id": "ep_jkl012",
      "submitted_at": "2025-01-15T11:00:00Z",
      "submitted_by_patient": true,
      "is_pre_booking": false,
      "created_at": "2025-01-15T11:00:00Z",
      "updated_at": "2025-01-15T11:00:00Z"
    },
    "previous": null
  }
}

The responses field (containing actual form answers) is intentionally excluded from webhook payloads for clinical data protection. Fetch the full response via the API after receiving the notification.

Payload Fields

FieldTypeDescription
idstringUnique identifier for this event
typestringThe event type (e.g., appointment.created)
created_atstringISO 8601 timestamp when event occurred
organization_idstringYour organization ID
data.objectobjectCurrent state of the resource
data.previousobjectPrevious state (for update events only)

Setting Up Webhooks

1. Create a Webhook Endpoint

  1. Go to Settings > Webhooks in your Jump EHR dashboard
  2. Click Add Endpoint
  3. Enter your endpoint URL (must be HTTPS)
  4. Select the events you want to receive
  5. Click Create
⚠️

Your webhook secret is displayed only once when creating the endpoint. Copy and store it securely.

2. Configure Your Server

Your server must:

  • Accept POST requests
  • Respond with a 2xx status code within 30 seconds
  • Verify the webhook signature (recommended)

URL Requirements

  • Must use HTTPS (HTTP not allowed in production)
  • Cannot point to internal/private IP addresses (127.x, 10.x, 172.16-31.x, 192.168.x)
  • Must be publicly accessible

Security

Signature Verification

Every webhook request includes a signature header for verification:

X-Webhook-Signature: t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

The signature format is t=<timestamp>,v1=<signature> where:

  • t is the Unix timestamp when the webhook was sent
  • v1 is the HMAC-SHA256 signature

Verifying Signatures

To verify a webhook signature:

  1. Extract the timestamp and signature from the header
  2. Construct the signed payload: {timestamp}.{request_body}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare with the provided signature

Node.js Example

const crypto = require('crypto');
 
function verifyWebhookSignature(payload, signature, secret) {
  const [tPart, v1Part] = signature.split(',');
  const timestamp = tPart.split('=')[1];
  const providedSignature = v1Part.split('=')[1];
 
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
 
  // Use timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(providedSignature),
    Buffer.from(expectedSignature)
  );
}
 
// Express.js example
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const payload = req.body.toString();
 
  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = JSON.parse(payload);
  console.log('Received event:', event.type);
 
  // Process the event
  switch (event.type) {
    case 'appointment.created':
      handleAppointmentCreated(event.data.object);
      break;
    case 'patient.updated':
      handlePatientUpdated(event.data.object, event.data.previous);
      break;
  }
 
  res.status(200).send('OK');
});

Python Example

import hmac
import hashlib
 
def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
    parts = dict(part.split('=') for part in signature.split(','))
    timestamp = parts['t']
    provided_signature = parts['v1']
 
    signed_payload = f"{timestamp}.{payload}"
    expected_signature = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
 
    return hmac.compare_digest(provided_signature, expected_signature)
 
# Flask example
@app.route('/webhooks', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Webhook-Signature')
    payload = request.get_data(as_text=True)
 
    if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401
 
    event = request.get_json()
    print(f"Received event: {event['type']}")
 
    # Process the event
    if event['type'] == 'appointment.created':
        handle_appointment_created(event['data']['object'])
 
    return 'OK', 200

Additional Headers

Each webhook request includes these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature for verification
X-Webhook-Event-IDUnique event ID for deduplication
X-Webhook-Event-TypeEvent type name
User-AgentJumpEHR-Webhooks/1.0
Content-Typeapplication/json

Delivery & Retries

Delivery Behavior

  • Webhooks are delivered within seconds of the event occurring
  • Each delivery attempt has a 30-second timeout
  • Your endpoint must respond with a 2xx status code to confirm receipt

Retry Schedule

If delivery fails, Jump EHR retries with exponential backoff:

AttemptDelay
11 minute
24 minutes
316 minutes
464 minutes (~1 hour)
5256 minutes (~4 hours)

After 5 failed attempts, the delivery is marked as failed and not retried.

Success Criteria

A delivery is considered successful when your endpoint:

  • Responds with HTTP status 200-299
  • Responds within 30 seconds

Return a 2xx response quickly, then process the webhook asynchronously. This prevents timeouts for long-running operations.

Best Practices

1. Respond Quickly

Acknowledge the webhook immediately and process asynchronously:

app.post('/webhooks', (req, res) => {
  // Respond immediately
  res.status(200).send('OK');
 
  // Process asynchronously
  processWebhookAsync(req.body).catch(console.error);
});

2. Handle Duplicates

Use the X-Webhook-Event-ID header to detect and skip duplicate deliveries:

const processedEvents = new Set();
 
function handleWebhook(event, eventId) {
  if (processedEvents.has(eventId)) {
    console.log('Duplicate event, skipping');
    return;
  }
 
  processedEvents.add(eventId);
  // Process event...
}

3. Verify Signatures

Always verify webhook signatures in production to ensure requests are from Jump EHR.

4. Log Webhook Events

Keep logs of received webhooks for debugging and auditing:

function logWebhook(event, status) {
  console.log({
    timestamp: new Date().toISOString(),
    event_id: event.id,
    event_type: event.type,
    status: status
  });
}

Testing Webhooks

Test Endpoint

Use the Test button in the webhook settings to send a test event:

{
  "id": "evt_test_123",
  "type": "test.ping",
  "created_at": "2025-01-15T10:30:00Z",
  "organization_id": "org_xyz789",
  "data": {
    "object": {
      "message": "This is a test webhook"
    },
    "previous": null
  }
}

Local Development

For local development, use a tunneling service like ngrok to expose your local server:

ngrok http 3000

Then use the ngrok URL as your webhook endpoint during development.

Viewing Delivery History

View webhook delivery history in Settings > Webhooks > select your endpoint > Delivery Log.

The log shows:

  • Event type and ID
  • Delivery status (delivered, failed, pending)
  • Response status code
  • Number of attempts
  • Timestamps

Next Steps