Webhook Processing Pipeline
This document traces the complete path of a PMS event from HTTP request to device access — every step, every component, and every decision point.
Components involved
| Component | Responsibility |
|---|---|
mosler-webhook | HTTP ingestion, validation, normalisation, persistence, RabbitMQ publish |
| RabbitMQ | Message queue and dead-letter exchange for retries |
mosler-worker | Business logic, user resolution, booking management, device access provisioning |
device-interface-lib | Hardware abstraction — creates eKeys, passcodes, and RFID card access |
| MongoDB | Persists both the raw PMS event (pms_inbound_events) and the resulting booking (bookings_v3) |
Phase 1 — Webhook Service (mosler-webhook)
Step 1: Header validation
The request must include X-Company-Id and X-Site-Id. Missing headers return 400 immediately, before any payload parsing.
Step 2: Provider routing
The :provider path segment is validated against the list of supported providers (generic, ezee, hotelogix). An invalid provider returns 400.
Step 3: Payload validation
The provider-specific adapter validates required fields and rejects the request with a 400 if validation fails. For Mosler Direct, required fields are reference_id, action, and at least one date pair. For eZee, required fields are hotel_code, the booking transaction ID, operation, and both dates.
Step 4: Normalisation
The adapter maps provider-specific field names into Mosler's canonical IPmsInboundBookingEvent structure. All field names use snake_case. Action strings are mapped to typed event enum values (BOOKING_CREATE, etc.).
Step 5: Duplicate detection
Before persisting, the service queries MongoDB for any event with the same company_id, reference_id, and event_type received within the last 60 seconds. If a duplicate is found:
- The incoming event is stored with
status = DUPLICATE(for audit trail) - The original event's ID and status are returned immediately
- No new RabbitMQ message is published
This prevents double-processing when a PMS retries a failed HTTP request.
Step 6: Persistence
The normalised event is saved to MongoDB (pms_inbound_events collection) with status = RECEIVED.
Step 7: RabbitMQ publish
The event is published to the pms.booking.events exchange. On success, status is updated to QUEUED and a 202 Accepted is returned to the PMS. On failure, status is set to FAILED and a 400 is returned (so the PMS knows to retry).
Phase 2 — Worker (mosler-worker)
Step 8: Consume from queue
The worker subscribes to the queue bound to pms.booking.events. Each message is acknowledged only after successful processing (or permanently rejected after max retries).
Step 9: Event type dispatch
Based on event_type, the message is routed to the appropriate handler:
| Event type | Handler |
|---|---|
BOOKING_CREATE | executeCreate() |
BOOKING_UPDATE | executeUpdate() |
BOOKING_CANCEL | executeCancel() |
BOOKING_CHECKIN | executeCheckin() |
BOOKING_CHECKOUT | executeCheckout() |
BOOKING_ROOMMOVE | executeRoomMove() |
BOOKING_DATECHANGE | executeDateChange() |
Step 10: Idempotency check
For create events, the worker first queries bookings_v3 by company_id + reference_id. If a booking already exists, it skips processing and publishes a status update (COMPLETED) without creating a duplicate. This guard covers the case where the worker restarts mid-flight and reprocesses an already-completed message.
Step 11: User resolution
The worker runs the user resolution algorithm — parallel phone and email lookups, duplicate disambiguation, contact-info merging, or account creation. See the full algorithm in User Resolution Architecture.
Step 12: Room lookup
The room is found by room_number within the resolved site_id. If no room is found, processing fails and the event transitions to FAILED.
Step 13: Booking creation / update / cancellation
The appropriate booking record is created or mutated in bookings_v3 with the resolved user, room, dates, and access type.
Step 14: Device access provisioning
For create/checkin events, the worker calls GrantBookingAccessUseCase, which uses device-interface-lib to communicate with the lock hardware:
EKEY→ creates a mobile (Bluetooth) key for the guest via the lock vendor's APIPASSCODE→ generates a time-limited PINCARD→ programs the RFID card ID onto the lockMULTI→ all of the above
The lock vendor is abstracted behind device-interface-lib, so the same provisioning logic works across manufacturers.
For cancel/checkout events, access is revoked.
Step 15: Status update
After successful processing, the worker publishes a domain event that transitions the PMS inbound event record from QUEUED → COMPLETED.
Retry & failure handling
Processing fails
│
▼
Worker nacks message (false, false) → message goes to DLQ
│
▼
DLQ consumer re-queues with incremented retry_count
│
├── retry_count < 3 → reprocessed
│
└── retry_count ≥ 3 → message permanently rejected
event status → DEAD_LETTER
Retries use RabbitMQ's x-dead-letter-exchange with a delay. Each retry attempt increments retry_count on both the RabbitMQ message headers and the MongoDB event record.
Observability
| Signal | Where |
|---|---|
| Event status transitions | MongoDB pms_inbound_events.status field |
| Processing logs | Winston → AWS CloudWatch |
| Event counter by type and outcome | Prometheus mosler_worker_events_total{event_type, status} |
| DLQ depth gauge | Prometheus mosler_dlq_messages{queue} |
| Alerts | Slack / Telegram via DlqMonitorService when queue depth exceeds threshold |
| Distributed traces | OpenTelemetry → Grafana Tempo |
All logs include eventId and correlationId for end-to-end trace correlation between the webhook service and the worker.