Events
| Event type | Fires when |
|---|---|
appointment.created | Any new appointment, from any ConnectAI channel |
appointment.rescheduled | Time, mode or location changes |
appointment.cancelled | The appointment is cancelled |
appointment.completed | The appointment is marked completed |
appointment.no_show | The patient did not attend |
appointment.payment_updated | Payment captured, or the balance due changes |
patient.created | A new patient record originates in ConnectAI |
document.delivered | A report or prescription reached the patient |
document.failed | We could not deliver one. statusReason says why |
You subscribe to a subset when your key is issued. Unsubscribed types are never sent.
Request we send
POST https://your-system.example.com/connectai/webhook
Content-Type: application/json
X-ConnectAI-Key: cai_live_9f2c4a1b8e7d3c06
X-ConnectAI-Timestamp: 1758614400
X-ConnectAI-Signature: v1=6b8d…c31f
X-ConnectAI-Event-Id: 9a7f1c20-5db4-4f31-8e02-2b6c9d77a145
X-ConnectAI-Event-Type: appointment.created
X-ConnectAI-Delivery-Attempt: 1Body
{
"eventId": "9a7f1c20-5db4-4f31-8e02-2b6c9d77a145",
"eventType": "appointment.created",
"occurredAt": "2026-09-24T14:32:07.412Z",
"clinicId": "68d63629fdec0a56e25e0a6d",
"appointment": {
"connectaiId": "68f04b1177ce0a1d2e9b4410",
"externalId": null,
"startsAt": "2026-09-25T04:45:00.000Z",
"startsAtLocal": "2026-09-25T10:15:00+05:30",
"mode": "Offline",
"status": "Approved",
"token": "A-14",
"notes": "Follow-up",
"payment": {
"type": "Partial",
"amountPaid": 200,
"balanceDue": 600,
"currency": "INR",
"payAtClinic": true
}
},
"doctor": { "connectaiId": "66a1…", "externalId": "HMS-DR-114", "name": "Dr Anita Rao" },
"patient": { "connectaiId": "68a9…", "externalId": "HMS-PT-88213", "name": "Rohan Mehta", "phone": "+919811223344" },
"location": { "connectaiId": "67b2…", "externalId": "HMS-LOC-2", "name": "Gomti Nagar" }
}Every entity carries both identifiers. externalId is null when the record originated in ConnectAI and you have not claimed it yet — create your own record, then send us the mapping.
Times appear twice deliberately: startsAt in UTC is authoritative, startsAtLocal is for display. Integrate against startsAt.
Verifying us
Node.js / Express
// Express — capture the raw body so the signature can be checked.
app.use("/connectai/webhook", express.json({
verify: (req, _res, buf) => { req.rawBody = buf; },
}));
app.post("/connectai/webhook", (req, res) => {
const timestamp = req.headers["x-connectai-timestamp"];
const provided = req.headers["x-connectai-signature"];
const expected =
"v1=" +
crypto.createHmac("sha256", OUTBOUND_SECRET)
.update(`${timestamp}.${req.rawBody.toString("utf8")}`, "utf8")
.digest("hex");
const a = Buffer.from(expected), b = Buffer.from(String(provided));
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
// Persist first, then return 200. Do the work afterwards.
enqueue(req.body);
res.sendStatus(200);
});What you must return
Any 2xx, with any body, within 10 seconds.
Return 200 the moment you have persisted the event, BEFORE you process it. If you do your business logic inside the webhook handler and time out, we will treat it as a failure and redeliver — and you will process it twice.
Retries and ordering
- Delivery is at-least-once. Deduplicate on
eventId. - Retry ladder: 30s, 2m, 10m, 1h, 6h, 12h, 24h. Seven attempts over roughly 44 hours, then we stop and alert our team.
- Deliveries for one appointment are ordered. Different appointments may arrive out of order.
- Every payload is a complete current-state snapshot, not a diff, so a late-arriving older event is safe to discard by comparing
occurredAt. - After 50 consecutive failures we pause your queue rather than keep hammering a dead endpoint. Queued events are kept and drain in order once it is resumed.