Skip to main content
Webhooks let your server receive automatic HTTP notifications the moment a clinical resource changes in ClinikAPI — no polling required. Every delivery is signed with HMAC-SHA256 so you can verify it came from ClinikAPI before processing it.

Setting up webhooks

1

Open the Webhooks panel

Go to Webhooks in the Developer Dashboard.
2

Create a webhook

Click Create Webhook and enter your endpoint URL. The URL must use HTTPS.
3

Choose your events

Select the events you want to receive. You can use wildcard patterns like patient.* or subscribe to all events with *.
4

Save and copy your secret

Click Save. ClinikAPI generates a webhook secret — copy it and store it as an environment variable. You’ll use it to verify signatures.

Event format

Events follow the {resource}.{action} pattern:
patient.created
encounter.updated
prescription.deleted

Payload structure

Each delivery sends a JSON body to your endpoint:
{
  "id": "evt_lx8f3a7x2_k9m2n4",
  "event": "patient.created",
  "timestamp": "2025-01-15T09:15:00.000Z",
  "data": {
    "resourceType": "patient",
    "resourceId": "pt_abc123",
    "action": "created",
    "tenantId": "org_def456"
  }
}

Request headers

ClinikAPI includes these headers on every webhook delivery:
HeaderDescription
X-Clinik-SignatureHMAC-SHA256 hex signature of the raw request body
X-Clinik-TimestampUnix timestamp (seconds) when the delivery was sent
X-Clinik-EventEvent type, e.g. patient.created
X-Clinik-Delivery-IdUnique delivery ID — use this for deduplication

Signature verification

Always verify the signature before processing the event. Use crypto.timingSafeEqual to prevent timing attacks:
import crypto from 'crypto';

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// In your webhook handler
app.post('/webhooks/clinikapi', (req, res) => {
  const signature = req.headers['x-clinik-signature'] as string;
  const isValid = verifyWebhook(req.body, signature, process.env.CLINIK_WEBHOOK_SECRET!);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  switch (event.event) {
    case 'patient.created':
      // Handle new patient
      break;
    case 'appointment.updated':
      // Handle appointment change
      break;
  }

  res.status(200).send('OK');
});
Parse the raw request body as a string before passing it to the verification function. If you parse JSON first and re-serialize it, the signature will not match.

Event subscription patterns

You can subscribe to all events, a resource family, or a single event type:
PatternDescription
*All events across all resource types
patient.*All patient events (created, updated, deleted)
patient.createdOnly patient creation events
Use specific subscriptions when possible to reduce unnecessary deliveries to your endpoint.

Available events

All 14 resource types emit .created, .updated, and .deleted events:
ResourceEvents
patientpatient.created, patient.updated, patient.deleted
encounterencounter.created, encounter.updated, encounter.deleted
observationobservation.created, observation.updated, observation.deleted
medicationmedication.created, medication.updated, medication.deleted
appointmentappointment.created, appointment.updated, appointment.deleted
intakeintake.created, intake.updated, intake.deleted
consentconsent.created, consent.updated, consent.deleted
lablab.created, lab.updated, lab.deleted
prescriptionprescription.created, prescription.updated, prescription.deleted
notenote.created, note.updated, note.deleted
assessmentassessment.created, assessment.updated, assessment.deleted
documentdocument.created, document.updated, document.deleted
practitionerpractitioner.created, practitioner.updated, practitioner.deleted
practitioner-rolepractitioner-role.created, practitioner-role.updated, practitioner-role.deleted

Retry policy

If your endpoint does not respond with a 2xx status within 30 seconds, ClinikAPI retries the delivery with exponential backoff:
AttemptDelay after previous failure
130 seconds
22 minutes
38 minutes
432 minutes
52 hours
After 5 failed attempts the delivery is marked as failed. You can review failed deliveries in Analytics > Webhook Logs in the Dashboard.

Best practices

  • Return 200 immediately and process the event asynchronously — slow handlers increase retry risk.
  • Use the X-Clinik-Delivery-Id header as an idempotency key so duplicate deliveries don’t cause side effects.
  • Verify the signature on every request, even from known IP ranges.
  • Subscribe to specific events rather than * to avoid processing events your app doesn’t need.