Notifications (Twilio, SendGrid, WhatsApp)
Nexa delivers notifications to passengers and operators across multiple channels, with delivery telemetry from each. The notifications domain is the single outbound communication surface for the platform — every other domain enqueues a notification request; the notifications domain decides how it gets delivered.
Channel matrix
| Channel | Vendor | Tenant scope | Delivery receipt | Open / read | 2-way |
|---|---|---|---|---|---|
| SMS | Twilio | Per-tenant phone numbers | Yes (status callback) | No | Optional |
| SendGrid | Shared platform | Yes (Event Webhook) | Yes (open + click) | No | |
| Meta WhatsApp Business | Per-tenant phone numbers | Yes (status webhook) | Yes (read receipts) | Yes | |
| Web Push | Web Push (VAPID) | Per-tenant subscription | Server-side delivery only | No | No |
| Airline app deep-link | Per-airline custom adapter | Per-tenant | Per integration | Per integration | Per integration |
The first three are the primary channels; Web Push is the PWA-resident channel for passengers who have the webapp installed; the airline-app deep-link is a per-airline custom adapter when an airline has its own consumer app and wants Nexa notifications to surface there.
Architecture
Triggers
A notification is dispatched on a trigger event. Examples:
| Trigger | Channel default | Recipient |
|---|---|---|
case.disruption.detected (auto) | SMS + WhatsApp + Email | Affected passengers |
subcase.offer-ready | SMS + WhatsApp + Email (with magic-link) | Lead passenger of the group |
voucher.issued | Email (with PDF voucher) | Lead passenger |
wallet.card-issued | SMS + Email + Push | Lead passenger |
wallet.card-loaded (scheduled drop) | Push (preferred) + SMS fallback | Lead passenger |
transport.dispatched | SMS + Push | Lead passenger |
subcase.failed | Operator UI + email to OPS_SUPERVISOR | Operators |
Template selection
A notification has three layers:
- Trigger event — what happened.
- Recipient segment — passenger tier, language, opt-ins, special-needs flags.
- Channel — picked by per-tenant policy + recipient preference + fallback chain.
The template store carries variants per (trigger, segment, channel, language) tuple. Selection logic:
template = templateStore.select({
trigger: "subcase.offer-ready",
segment: { tier: "BUSINESS", language: "es" },
channel: "whatsapp"
})
If a specific variant doesn't exist, the resolver falls back: tier-specific → tenant-default → platform-default. Language fallback: requested → es (Latin America default) → en.
Special-content notifications
Some notifications need extra content beyond the standard offer:
- Baggage instructions — when the disruption requires baggage re-collection at a different airport, the template includes the instructions and any reference numbers.
- Special-needs-aware messaging — for passengers with
WHEELCHAIR_REQUEST,INFANT,MEDICALflags, the template includes specific accommodation guidance.
These rules live in the template-selection layer; the content variants live in the template store.
Channel routing
Per-tenant policy declares the default channel chain per trigger:
case.disruption.detected → [whatsapp, sms, email] // try whatsapp first; fall back if undeliverable
subcase.offer-ready → [sms, whatsapp, email]
voucher.issued → [email] // email-only; voucher is HTML
wallet.card-loaded → [push, sms] // push if PWA installed; else SMS
Per-passenger preferences override the default chain — a passenger who unsubscribed from SMS receives email-only.
The dispatcher attempts the first channel; on confirmed delivery (or after the channel's grace period), it considers the notification delivered. On undeliverability (bounce, opted-out, no number on file), it falls back to the next channel.
Idempotency
Every dispatch carries a notification_urn. Replays of the same trigger (e.g., from at-least-once event delivery) find the existing dispatch and short-circuit — no duplicate sends.
Delivery telemetry
Every channel adapter exposes inbound webhooks for delivery state:
- Twilio —
delivered,failed,undelivered,replied. - SendGrid —
delivered,bounce,dropped,open,click,unsubscribe. - WhatsApp —
sent,delivered,read,failed. - Web Push — server-side delivery confirmation only (no read receipt).
Inbound webhooks are HMAC-validated, normalized into the canonical NotificationDeliveryEvent shape, and persisted to the per-tenant notifications store. The aggregated event stream is published on the workflow bus for downstream consumers (analytics, audit, partner subscribers).
The operator UI surfaces the delivery state per-passenger ("Delivered to WhatsApp 3m ago, read 2m ago, accepted offer 1m ago") so operators can see the full chain.
Bounce / opt-out handling
When a delivery webhook reports a hard bounce or opt-out:
- The recipient's contact preferences for that channel are flagged.
- Future dispatches for that recipient skip the flagged channel.
- The next available channel in the routing chain is used.
If every channel is unavailable, the operator UI surfaces a "passenger unreachable" warning on the case, prompting the operator to call the number directly or refer to airport-side communication.
Tenant-managed credentials
| Vendor | Credentials | Notes |
|---|---|---|
| Twilio | Per-tenant | Each airline has its own dedicated phone number(s) for branding and deliverability. |
| SendGrid | Nexa-managed | Bundled in the subscription. Sender domain is notify.<airline>.nexastudio.io for per-tenant branding. |
| Meta WhatsApp Business | Per-tenant | Each airline has its own WhatsApp Business phone number; templates need Meta-side approval per airline. |
| Web Push (VAPID) | Per-tenant | VAPID keys are per-tenant; subscriptions live in the per-tenant DB. |
SLA & latency
| Channel | Detection-to-first-message p95 |
|---|---|
| SMS (Twilio) | < 30 s |
| < 60 s | |
| Email (SendGrid) | < 60 s |
| Push | < 30 s |
Latency is measured from the subcase.offer-ready event (or equivalent trigger) to the channel-side delivery confirmation. Vendor-side latency outside Nexa's control is excluded.
Failure modes
| Failure | Action |
|---|---|
| Twilio 5xx | Retry with backoff; fall back to next channel after 3 attempts. |
| SendGrid bounce | Mark email invalid; fall back to next channel. |
| WhatsApp template rejected by Meta | Alert ops; cannot recover automatically. |
| Webhook signature invalid | Reject with 401; vendor retries. |
| Vendor outage > 5 min | Circuit breaker opens for the channel; operator UI surfaces partial delivery. |
Compliance
- All vendors are data sub-processors under Nexa's tenant DPAs.
- Phone numbers and emails sent to vendors are minimum-necessary.
- Opt-out / unsubscribe handling is mandatory per channel-specific regulation (CAN-SPAM, GDPR, LGPD).
- Delivery logs retain the message body for 30 days for support purposes; redacted summaries are retained for 12 months for analytics.
Onboarding checklist
For a new tenant:
- Tenant procures Twilio account + dedicated phone number(s).
- Tenant procures Meta WhatsApp Business account + phone number; Meta-approves notification templates.
- Nexa configures SendGrid sender domain
notify.<airline>.nexastudio.io. - Webhook endpoints registered with each vendor (per-tenant).
- Default channel routing configured per tenant policy.
- First end-to-end notification validated against sandbox case.