Appointment Webhooks
Receive real-time event notifications when appointments are created, updated, cancelled, or completed, and when calendar windows change.
Webhook endpoints are registered and managed through the HuskyVoice Dashboard — go to Dashboard → Integrations → Webhooks → Add Endpoint and select the appointment events you want to receive.
Important: API Writes Do Not Fire Webhooks
Appointment and slot events are not dispatched for writes made through the REST API. Webhooks fire only when changes originate from:
- The HuskyVoice dashboard
- The AI voice agent (appointments booked during a call)
If your integration creates or updates appointments via the REST API and you need those changes reflected downstream, handle that in your own application code rather than relying on webhook delivery.
Registerable Events
| Event | When it fires |
|---|---|
appointment.created | An appointment is booked via the dashboard or by the AI voice agent |
appointment.updated | An appointment's details are changed via the dashboard |
appointment.cancelled | An appointment is cancelled via the dashboard |
appointment.completed | An appointment is marked completed via the dashboard |
slot.created | A calendar window is created via the dashboard |
slot.updated | A calendar window is updated via the dashboard |
Event Payloads
appointment.* Events
All four appointment events use the same payload shape.
{
"event_id": "evt_a1b2c3d4-...",
"event": "appointment.created",
"timestamp": "2026-05-26T10:00:00.000Z",
"appointment": {
"appointment_id": "appt_a1b2c3d4e5",
"appointment_type": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"appointment_type_name": "General Checkup",
"branch_id": "branch_uuid_here",
"date": "2026-06-15",
"start_time": "2026-06-15T04:00:00.000Z",
"session": "Morning",
"batch": "MA",
"token": null,
"patient_name": "Aadhi",
"parent_phone": "+919840XXXXXX",
"doctor_name": null,
"status": "confirmed",
"booked_via": "phone_call",
"booked_at": "2026-05-26T10:00:00.000Z"
}
}
| Field | Description |
|---|---|
event_id | Unique identifier for this event delivery |
event | Event type: appointment.created, appointment.updated, appointment.cancelled, or appointment.completed |
timestamp | ISO 8601 UTC datetime when the event was dispatched |
appointment.appointment_id | Appointment identifier |
appointment.appointment_type | Service UUID |
appointment.appointment_type_name | Human-readable service name |
appointment.branch_id | Branch UUID |
appointment.date | Appointment date — YYYY-MM-DD |
appointment.start_time | Full ISO 8601 UTC datetime of the appointment |
appointment.session | Session name |
appointment.batch | Batch label |
appointment.token | Queue token, if assigned |
appointment.patient_name | Patient name |
appointment.parent_phone | Patient phone number |
appointment.doctor_name | Assigned doctor, if set |
appointment.status | Current appointment status |
appointment.booked_via | How the appointment was created: "phone_call" for AI agent, "dashboard" for manual |
appointment.booked_at | ISO 8601 UTC datetime the appointment was created |
slot.* Events
Both slot events use the same payload shape.
{
"event_id": "evt_a1b2c3d4-...",
"event": "slot.updated",
"timestamp": "2026-05-26T10:00:00.000Z",
"slot": {
"slot_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890||2026-06-15",
"branch_id": "branch_uuid_here",
"appointment_type_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"appointment_type_name": "General Checkup",
"date": "2026-06-15",
"day_of_week": "monday",
"session": "Morning",
"batch": "MA",
"start_time": "09:00",
"end_time": "11:00",
"max_slots": 20,
"filled_slots": 3,
"available_slots": 17,
"status": "OPEN"
}
}
Verifying Signatures
When require_signature is true, every delivery includes two headers:
| Header | Description |
|---|---|
X-Webhook-Timestamp | Unix timestamp (seconds) when the payload was signed |
X-Webhook-Signature | Signature in the format v1=<base64-encoded HMAC-SHA256> |
The signature is computed as:
HMAC-SHA256(secret, "{timestamp}.{rawBody}")
encoded as base64 (not hex), and prefixed with v1=.
- cURL
- Python
- JavaScript
- n8n
# Generate a signed test delivery to verify your endpoint
SECRET="YOUR_WEBHOOK_SECRET"
TIMESTAMP=$(date +%s)
PAYLOAD='{"event_id":"evt_test","event":"appointment.created","timestamp":"2026-06-01T10:00:00.000Z","appointment":{"appointment_id":"appt_test"}}'
SIG="v1=$(printf '%s.%s' "$TIMESTAMP" "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)"
curl -X POST https://your-server.com/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Timestamp: $TIMESTAMP" \
-H "X-Webhook-Signature: $SIG" \
-d "$PAYLOAD"
import hmac
import hashlib
import base64
def verify_signature(secret: str, timestamp: str, raw_body: bytes, signature: str) -> bool:
message = f"{timestamp}.".encode() + raw_body
expected = base64.b64encode(
hmac.new(secret.encode(), message, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(f"v1={expected}", signature)
const crypto = require("crypto");
function verifySignature(secret, timestamp, rawBody, signature) {
const message = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(message)
.digest("base64");
return crypto.timingSafeEqual(
Buffer.from(`v1=${expected}`),
Buffer.from(signature)
);
}
// n8n Code node — place after your Webhook trigger node
const crypto = require("crypto");
const secret = "YOUR_WEBHOOK_SECRET";
const timestamp = $input.first().json.headers["x-webhook-timestamp"];
const signature = $input.first().json.headers["x-webhook-signature"];
const rawBody = JSON.stringify($input.first().json.body);
const message = `${timestamp}.${rawBody}`;
const expected =
"v1=" +
crypto.createHmac("sha256", secret).update(message).digest("base64");
const isValid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
if (!isValid) {
throw new Error("Invalid webhook signature — request rejected");
}
return $input.all();
Always use a timing-safe comparison (e.g. hmac.compare_digest or crypto.timingSafeEqual) to prevent timing attacks. Reject payloads where the timestamp is more than a few minutes old.
Delivery & Retries
Webhooks time out after 8 seconds. A 2xx response is required to mark a delivery as successful. Failed deliveries are retried up to 5 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
After 50 consecutive failures, the webhook is automatically disabled. Re-enable it from Dashboard → Integrations → Webhooks.
To inspect recent delivery attempts or replay a failed delivery, use the delivery history view in Dashboard → Integrations → Webhooks → your endpoint → Deliveries.