Skip to content

Webhook Events Catalog

All event names follow the {entity}.{action} convention.

Event Types

EventTriggerData Included
appointment.createdNew appointment created (from template or public booking)appointment_id, specialist_id, patient_id, status, scheduled_at
appointment.updatedStatus change, reschedule, or field updateappointment_id, specialist_id, patient_id, status, scheduled_at
appointment.cancelledAppointment cancelledappointment_id, specialist_id, patient_id, reason
appointment.noshowPatient marked as no-showappointment_id, specialist_id, patient_id
patient.onboardedNew patient created via onboarding flowpatient_id
patient.updatedPatient profile updatedpatient_id
form.submittedForm instance submitted (status → completed)form_id, appointment_id, patient_id, specialist_id
form.signedForm instance signed (status → signed)form_id, appointment_id, patient_id, specialist_id
document.generatedReport or prescription PDF generateddocument_id, appointment_id, type (report/prescription)
document.signedDocument digitally signeddocument_id, appointment_id, type
segment.membership_changedPatient added to or removed from a segmentsegment_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:

json
{
  "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 event
  • data — Event-specific payload (see schemas below)

Event Schemas

appointment.created

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "id": "evt_01HQ3K5M7N8P9R0S",
  "event": "patient.onboarded",
  "timestamp": "2025-07-15T14:30:00Z",
  "organization_id": 42,
  "data": {
    "patient_id": 78,
    "user_id": 123
  }
}

patient.updated

json
{
  "id": "evt_01HQ3K5M7N8P9R0S",
  "event": "patient.updated",
  "timestamp": "2025-07-15T14:30:00Z",
  "organization_id": 42,
  "data": {
    "patient_id": 78
  }
}

form.submitted

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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

json
{
  "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 segment
  • removed — 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:

  1. Eliminate BAA requirements — No need for Business Associate Agreements with automation platforms
  2. Prevent PII leaks — If a webhook URL is misconfigured, no sensitive data is exposed
  3. 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:

  1. Receive the webhook event (contains IDs only)
  2. Call the Core API's authenticated API to fetch details
  3. Use the entity IDs from the webhook payload

Example:

javascript
// 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

json
{
  "url": "https://your-endpoint.com/webhook",
  "events": ["appointment.created", "appointment.cancelled"]
}

Subscribe to All Events

Use the wildcard "*":

json
{
  "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:

go
// 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

go
// 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.