Webhook Events Catalog
All event names follow the {entity}.{action} convention.
Event Types
| Event | Trigger | Data Included |
|---|---|---|
appointment.created | New appointment created (from template or public booking) | appointment_id, specialist_id, patient_id, status, scheduled_at |
appointment.updated | Status change, reschedule, or field update | appointment_id, specialist_id, patient_id, status, scheduled_at |
appointment.cancelled | Appointment cancelled | appointment_id, specialist_id, patient_id, reason |
appointment.noshow | Patient marked as no-show | appointment_id, specialist_id, patient_id |
patient.onboarded | New patient created via onboarding flow | patient_id |
patient.updated | Patient profile updated | patient_id |
form.submitted | Form instance submitted (status → completed) | form_id, appointment_id, patient_id, specialist_id |
form.signed | Form instance signed (status → signed) | form_id, appointment_id, patient_id, specialist_id |
document.generated | Report or prescription PDF generated | document_id, appointment_id, type (report/prescription) |
document.signed | Document digitally signed | document_id, appointment_id, type |
segment.membership_changed | Patient added to or removed from a segment | segment_id, patient_id, action (added/removed) |
New event types can be added as features are built. Subscribers using {"*"} receive all events automatically.
Payload Format
Every webhook delivery is a POST with Content-Type: application/json.
Standard Envelope
All events follow this structure:
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "appointment.created",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
// Event-specific payload
}
}Envelope fields:
id— Unique event identifier (use for idempotency)event— Event type (e.g.,appointment.created)timestamp— When the event occurred (ISO 8601 UTC)organization_id— Organization that generated the eventdata— Event-specific payload (see schemas below)
Event Schemas
appointment.created
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "appointment.created",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"appointment_id": 1234,
"specialist_id": 56,
"patient_id": 78,
"specialty_id": 3,
"appointment_template_id": 12,
"status": "upcoming",
"scheduled_at": "2025-07-20T10:00:00Z"
}
}appointment.updated
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "appointment.updated",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"appointment_id": 1234,
"specialist_id": 56,
"patient_id": 78,
"status": "confirmed",
"scheduled_at": "2025-07-20T15:00:00Z",
"changed_fields": ["status", "scheduled_at"]
}
}appointment.cancelled
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "appointment.cancelled",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"appointment_id": 1234,
"specialist_id": 56,
"patient_id": 78,
"reason": "Patient request"
}
}appointment.noshow
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "appointment.noshow",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"appointment_id": 1234,
"specialist_id": 56,
"patient_id": 78,
"scheduled_at": "2025-07-15T10:00:00Z"
}
}patient.onboarded
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "patient.onboarded",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"patient_id": 78,
"user_id": 123
}
}patient.updated
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "patient.updated",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"patient_id": 78
}
}form.submitted
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "form.submitted",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"form_id": 456,
"form_template_id": 12,
"appointment_id": 1234,
"patient_id": 78,
"specialist_id": 56,
"status": "completed"
}
}form.signed
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "form.signed",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"form_id": 456,
"form_template_id": 12,
"appointment_id": 1234,
"patient_id": 78,
"specialist_id": 56,
"status": "signed",
"signed_at": "2025-07-15T14:30:00Z"
}
}document.generated
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "document.generated",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"document_id": 789,
"appointment_id": 1234,
"form_id": 456,
"type": "report"
}
}document.signed
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "document.signed",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"document_id": 789,
"appointment_id": 1234,
"type": "prescription",
"signed_at": "2025-07-15T14:30:00Z"
}
}segment.membership_changed
{
"id": "evt_01HQ3K5M7N8P9R0S",
"event": "segment.membership_changed",
"timestamp": "2025-07-15T14:30:00Z",
"organization_id": 42,
"data": {
"segment_id": 10,
"patient_id": 78,
"action": "added"
}
}Action values:
added— Patient added to segmentremoved— Patient removed from segment
PII Policy
Webhook payloads contain NO personally identifiable information.
What's Included
- Entity IDs (
appointment_id,patient_id,specialist_id, etc.) - Status values (
upcoming,completed,signed, etc.) - Timestamps (ISO 8601 UTC)
- Enum values (
action,type, etc.)
What's NEVER Included
- Names
- Email addresses
- Phone numbers
- Physical addresses
- Medical data
- Form field values
- Custom field values
- Document content
- PHI of any kind
Why This Policy?
Webhook URLs are customer-controlled. We cannot guarantee the receiver's security posture. By sending only IDs, we:
- Eliminate BAA requirements — No need for Business Associate Agreements with automation platforms
- Prevent PII leaks — If a webhook URL is misconfigured, no sensitive data is exposed
- Enable secure integrations — Receivers call back to the Core API's authenticated API if they need details
Getting Full Data
If your integration needs patient names, form values, or other sensitive data:
- Receive the webhook event (contains IDs only)
- Call the Core API's authenticated API to fetch details
- Use the entity IDs from the webhook payload
Example:
// Webhook received
{
"event": "appointment.created",
"data": {
"appointment_id": 1234,
"patient_id": 78
}
}
// Fetch full details via API
const appointment = await fetch('https://api.example.com/v1/appointments/1234', {
headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
}).then(r => r.json());
// Now you have full appointment data (name, time, etc.)
console.log(appointment.patient.name); // "John Doe"Subscribing to Events
Subscribe to Specific Events
{
"url": "https://your-endpoint.com/webhook",
"events": ["appointment.created", "appointment.cancelled"]
}Subscribe to All Events
Use the wildcard "*":
{
"url": "https://your-endpoint.com/webhook",
"events": ["*"]
}Wildcard subscriptions automatically receive new event types as they're added to the Core API.
Event Emission
Events are emitted by domain services via the Emitter interface:
// internal/core/webhook/emitter.go
type Emitter struct {
db *pgxpool.Pool
}
// Emit queues a webhook event for async delivery to all matching subscriptions.
func (e *Emitter) Emit(ctx context.Context, orgID int64, eventType string, data any) error {
payload, _ := json.Marshal(map[string]any{
"id": "evt_" + generateID(),
"event": eventType,
"timestamp": time.Now().UTC().Format(time.RFC3339),
"organization_id": orgID,
"data": data,
})
// Find all active subscriptions for this org that match this event type
_, err := e.db.Exec(ctx, `
INSERT INTO webhook_events (organization_id, subscription_id, event_type, payload, status, next_retry_at)
SELECT $1, ws.id, $2, $3, 'pending', now()
FROM webhook_subscriptions ws
WHERE ws.organization_id = $1
AND ws.is_active = TRUE
AND ($2 = ANY(ws.events) OR '*' = ANY(ws.events))
`, orgID, eventType, payload)
return err
}Usage in Domain Code
// internal/core/domain/appointment/service.go
func (s *Service) Cancel(ctx context.Context, id int64, reason string) error {
appt, err := s.store.Cancel(ctx, id, reason)
if err != nil {
return err
}
// Side effects
s.dailyco.DeleteRoom(ctx, appt.DailycoRoomName)
// Emit webhook event (async — inserts rows, worker delivers later)
s.webhookEmitter.Emit(ctx, appt.OrganizationID, "appointment.cancelled", map[string]any{
"appointment_id": appt.ID,
"specialist_id": appt.SpecialistID,
"patient_id": appt.PatientID,
"reason": reason,
})
return nil
}Events are delivered asynchronously by the webhook worker.