Skip to content

Mailgun events

Mailgun fires webhook events for every email’s full lifecycle. We verify, dedup, store, and use these to power the delivery watchdog + per-booking timeline.

Events received

EventMeaningTerminal?
acceptedMailgun queued for deliveryNo
deliveredRecipient’s mail server acceptedYes (success)
failedSend failed (severity=permanent or temporary)Yes (failure)
rejectedMailgun pre-emptively dropped (suppression list, etc.)Yes (failure)
openedRecipient opened the email (tracking pixel)No
clickedRecipient clicked a tracked linkNo
unsubscribedRecipient hit the unsubscribe linkNo
complainedRecipient marked as spamYes (failure)

Webhook handler

/webhook/mailgun (src/routes/webhook-mailgun.ts):

  1. Parse JSON body
  2. Verify signature: HMAC-SHA256 of signature.timestamp + signature.token keyed by MAILGUN_WEBHOOK_SIGNING_KEY, hex compared with signature.signature
  3. Reject if timestamp skew > 15min (replay defense)
  4. Insert into mailgun_events (dedup on event_id unique index)
  5. Sync terminal-state to outbound_log:
    • deliveredmarkOutboundDelivered
    • failed / rejectedmarkOutboundFailed with reason
  6. Alert on permanent bounce + spam complaint: SMS Bill (once per event_id, no re-alert on retries)

Tagging

Outgoing sends include h:X-Mailgun-Variables with {booking_id, purpose}. Mailgun echoes them back on every event as event-data.user-variables. We extract + store in the mailgun_events.booking_id + purpose columns.

This is what makes per-booking delivery views work — every event can be joined back to the booking that triggered the send.

Purposes in use

  • heads_up, t2_reminder, time_commit, escalation_* — cleaner touchpoints
  • pre_arrival_t14, t5, t3, t1, t0_codes, t0_nudge
  • addon_early_checkin_confirmed/declined, addon_late_checkout_*
  • cohost_new_booking
  • bill_arrival_day, bill_departure_day, bill_dirty_at_3pm
  • form_bridge_pre_checkin/checkin/checkout
  • cleaner_cancel_notify, late_checkout_cleaner_notify
  • eta_miss, no_cleaner_alert, ack_confirm, complete_confirm
  • clean_issue, sms_forward, guest_checkout_eve
  • ota_airbnb_confirm, checkin_form_chase
  • daily_summary, t0_missing_body, addon_combined_deferral

Schema (mailgun_events)

event_id, event, recipient, message_id, domain, event_ts,
reason, severity, booking_id, purpose, raw, received_at

raw is the full event-data JSON for forensic queries.

Source

  • src/routes/webhook-mailgun.ts — receiver
  • src/lib/mailgun-signature.ts — HMAC helper
  • src/mailgun/client.tssendEmail adds variables + records to outbound_log on send
  • migrations/0022_mailgun_events.sql, 0023_mailgun_event_tagging.sql
  • Tests: test/routes/webhook-mailgun.test.ts (19 cases) + test/lib/mailgun-signature.test.ts (8 cases)