Skip to content

Webhooks & Events

Webhooks & Events

DELPHOS delivers real-time notifications for scheduling events through a webhook system built on the transactional outbox pattern. Webhook payloads are signed with HMAC-SHA256 and delivered with automatic retries and dead-letter handling.


Available Event Types

Event TypeTrigger
appointment.createdNew appointment booked
appointment.confirmedAppointment confirmed by staff
appointment.cancelledAppointment cancelled
appointment.rescheduledAppointment moved to a new time slot
appointment.completedAppointment finished successfully
appointment.no_showPatient did not appear
appointment.checked_inPatient arrived and checked in
slot.releasedCalendar slot became available

Create a Webhook Subscription

Register a callback URL to receive scheduling events.

POST /v1/scheduling/webhooks
FieldTypeRequiredDefaultDescription
urlstringYesTarget callback URL (HTTPS recommended)
secretstringYesHMAC-SHA256 shared secret (min 32 characters)
event_typesarrayYesEvent types to subscribe to (at least one)
descriptionstringNonullHuman-readable description
metadataobjectNo{}Additional metadata
CodeMeaning
201Webhook subscription created
400Secret too short or invalid event types
422Validation error
Terminal window
curl -X POST "https://your-instance.delphos.app/v1/scheduling/webhooks" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/scheduling",
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"event_types": [
"appointment.created",
"appointment.confirmed",
"appointment.cancelled",
"appointment.completed"
],
"description": "Production appointment notifications"
}'

Manage Subscriptions

List subscriptions

GET /v1/scheduling/webhooks
ParameterTypeDefaultDescription
limitinteger20Records per page (max 100)
offsetinteger0Pagination offset

Get a subscription

GET /v1/scheduling/webhooks/{webhook_id}

Update a subscription

PUT /v1/scheduling/webhooks/{webhook_id}

Only provided fields are updated. The shared secret cannot be changed through this endpoint.

FieldTypeRequiredDescription
urlstringNoNew callback URL
event_typesarrayNoNew event type subscriptions
is_activebooleanNoEnable or disable the subscription
descriptionstringNoUpdated description
metadataobjectNoUpdated metadata

Deactivate a subscription

DELETE /v1/scheduling/webhooks/{webhook_id}

Performs a soft-delete by setting is_active to false. The webhook record is preserved, but pending deliveries for deactivated webhooks are skipped by the dispatcher.


Webhook Payload

Every webhook delivery includes these HTTP headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature: sha256={hex_digest}
X-Webhook-TimestampUnix timestamp (seconds) of delivery
X-Webhook-Event-IdIdempotency UUID for the event
X-Webhook-Event-TypeEvent type (e.g. appointment.confirmed)
Content-TypeAlways application/json
User-AgentDELPHOS-Webhooks/1.0

Payload schema

The JSON body contains the appointment data at the time of the event:

{
"id": "550e8400-e29b-41d4-a716-446655440001",
"provider_profile_id": "550e8400-e29b-41d4-a716-446655440002",
"patient_id": "550e8400-e29b-41d4-a716-446655440003",
"appointment_type_id": "550e8400-e29b-41d4-a716-446655440004",
"location_id": "550e8400-e29b-41d4-a716-446655440005",
"start_time": "2026-05-15T09:00:00+00:00",
"end_time": "2026-05-15T09:30:00+00:00",
"status": "confirmed",
"source": "web",
"version": 2
}
FieldTypeDescription
idUUIDAppointment UUID
provider_profile_idUUIDProvider who owns the appointment
patient_idUUIDPatient UUID
appointment_type_idUUIDAppointment type UUID
location_idUUIDLocation UUID
start_timedatetimeAppointment start (ISO 8601)
end_timedatetimeAppointment end (ISO 8601)
statusstringCurrent appointment status
sourcestringBooking channel
versionintegerOptimistic concurrency version

Signature Verification

DELPHOS signs every payload with HMAC-SHA256 using your shared secret. The signature message format is {timestamp}.{json_payload}, which binds the timestamp to the payload to prevent replay attacks.

  1. Extract X-Webhook-Timestamp and X-Webhook-Signature from request headers.
  2. Construct the signed message: {timestamp}.{raw_body}.
  3. Compute HMAC-SHA256 using your shared secret.
  4. Compare using constant-time comparison to prevent timing attacks.
  5. Optionally reject events where the timestamp is older than 5 minutes.
import hashlib
import hmac
import time
def verify_webhook(
raw_body: bytes,
secret: str,
signature_header: str,
timestamp_header: str,
tolerance_seconds: int = 300,
) -> bool:
"""Verify a DELPHOS webhook signature.
Args:
raw_body: Raw request body bytes.
secret: Your shared secret.
signature_header: Value of X-Webhook-Signature header.
timestamp_header: Value of X-Webhook-Timestamp header.
tolerance_seconds: Max age of event (default 5 minutes).
Returns:
True if the signature is valid and timestamp is fresh.
"""
timestamp = int(timestamp_header)
# Reject stale events
if abs(time.time() - timestamp) > tolerance_seconds:
return False
# Reconstruct the signed message
message = f"{timestamp}.{raw_body.decode('utf-8')}"
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)

Retry Policy

Failed deliveries are retried with increasing delays:

AttemptDelayTotal elapsed
1st retry5 seconds~5s
2nd retry30 seconds~35s
3rd retry5 minutes~5m 35s

After all retry attempts are exhausted, the delivery is moved to the dead-letter queue. The default maximum is 3 attempts.

A delivery is considered successful when your endpoint returns an HTTP status code in the 2xx range. Any other response code, connection error, or timeout triggers a retry.

Delivery StatusMeaning
pendingAwaiting initial dispatch
successDelivered successfully (2xx response)
failedAttempt failed, retry scheduled
dead_letterAll retries exhausted

Test Events

Send a test event to verify webhook connectivity before going live.

POST /v1/scheduling/webhooks/{webhook_id}/test

Creates a webhook.test delivery that the background dispatcher picks up and delivers to your callback URL.

CodeMeaning
201Test event queued
404Webhook not found or inactive

Response:

{
"delivery_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event_type": "webhook.test",
"status": "pending"
}
Terminal window
curl -X POST "https://your-instance.delphos.app/v1/scheduling/webhooks/WEBHOOK_ID/test" \
-H "x-api-key: YOUR_API_KEY"

Delivery History

Inspect delivery attempts for debugging.

GET /v1/scheduling/webhooks/{webhook_id}/deliveries
ParameterTypeDefaultDescription
limitinteger20Records per page (max 100)
offsetinteger0Pagination offset

Each delivery record includes:

FieldTypeDescription
idUUIDDelivery UUID
webhook_idUUIDParent webhook subscription UUID
event_idUUIDIdempotency event UUID
event_typestringEvent type
payloadobjectEvent payload
statusstringpending, success, failed, dead_letter
attempt_countintegerNumber of delivery attempts
max_attemptsintegerMaximum allowed attempts
next_retry_atdatetimeNext retry timestamp (if retrying)
last_response_codeintegerLast HTTP status code received
last_response_bodystringLast HTTP response body (truncated to 1000 chars)
last_errorstringLast error message
delivered_atdatetimeSuccessful delivery timestamp
outbox_processedbooleanWhether the outbox entry has been dispatched
created_atdatetimeDelivery creation timestamp (UTC)
Terminal window
curl "https://your-instance.delphos.app/v1/scheduling/webhooks/WEBHOOK_ID/deliveries?limit=10" \
-H "x-api-key: YOUR_API_KEY"

Dead-Letter Queue

Deliveries that exhaust all retry attempts are moved to the dead-letter queue for manual inspection.

GET /v1/scheduling/webhooks/dead-letters

Review dead letters to identify recurring connectivity issues, incorrect callback URLs, or endpoints that consistently return errors.


Architecture

DELPHOS uses a transactional outbox pattern backed by a Redis retry queue:

+----------------+ +------------------+ +------------+
| API Action |---->| Outbox Table |---->| Dispatcher |
| (e.g. confirm) | | (same TX as | | (1s poll) |
| | | appointment) | | |
+----------------+ +------------------+ +------+-----+
|
+--------v--------+
| HTTP POST to |
| your endpoint |
+--------+--------+
|
+-------------v------------+
| 2xx? -> success |
| else -> Redis retry ZSET |
| (5s, 30s, 5m) |
| exhausted -> dead_letter |
+--------------------------+

Guarantees:

  • At-least-once delivery: Events are written to the outbox in the same database transaction as the appointment change. The dispatcher polls the outbox every second.
  • No redirects: The dispatcher does not follow HTTP redirects to prevent redirect-based SSRF.
  • Response size limit: Response bodies larger than 100KB are truncated in delivery logs.

Error Handling

CodeWhen
400Secret too short or invalid event types
404Webhook not found or inactive
422Validation error
500Internal server error

All error responses follow the standard format:

{
"detail": "Webhook a1b2c3d4-... not found"
}