Notification Service API
Outbound comms + CRM-lite. Delivery (SES + Twilio), contacts (leads + patients), campaigns (drips + scheduled + blasts), and preferences (HIPAA-aware). Replaces HubSpot entirely.
The notification service has been significantly expanded beyond the original delivery-only scope. New responsibilities: contacts (leads captured from intake, promoted to patients on patient.created), campaigns (event-triggered drips, scheduled sends, one-off blasts), campaign_enrollments, and messaging_preferences. HIPAA-aware category model: transactional (always sends), clinical (always sends at v1), marketing (freely opt-out per channel). HubSpot is fully retired — all contact, campaign, and drip functionality lives here now. Per-org verified senders (SES + Twilio) configured via organization_notification_config. Slack is no longer a standard channel; use it for dev/ops alerts only. See Architecture Overview for the current spec. Content below may reflect pre-expansion scope.
Overview
The Notification Service is a centralized API for all patient and internal notifications.
Instead of each service integrating directly with SES, Twilio, HubSpot, and Slack,
callers send a single POST /notify/send request and let the service handle
template resolution, channel routing, delivery, and logging.
Base URL
https://api.yourera.com/notify
How It Works
- Caller sends a
POST /notify/sendwith a notification type and recipient data - Service resolves the template for that notification type
- Template engine renders the template with provided variables
- Channel config determines which channels are enabled for this type
- Service dispatches to all enabled channels (email, SMS, HubSpot, Slack)
- Each channel delivery is independent - one failure doesn't block others
- All delivery attempts are logged with status, channel, and timestamps
Authentication
The /notify/send endpoint requires HMAC-SHA256 authentication.
Admin endpoints (/notify/templates/*, /notify/channels/*)
require an Admin JWT token.
HMAC Authentication (Send Endpoint)
| Header | Description |
|---|---|
X-API-Key |
Your public API key identifier |
X-Timestamp |
Current timestamp in ISO 8601 format. Must be within 5 minutes of server time. |
X-Signature |
HMAC-SHA256 hex digest of timestamp + '.' + jsonBody |
Signature Formula
HMAC-SHA256(apiSecret, timestamp + '.' + JSON.stringify(body))
Node.js Example
import crypto from 'node:crypto';
const timestamp = new Date().toISOString();
const body = JSON.stringify({
type: 'prescription_approved',
recipient: { email: 'patient@example.com', phone: '5551234567' },
variables: { patientName: 'Jane Doe', medication: 'Semaglutide' }
});
const signature = crypto
.createHmac('sha256', API_SECRET)
.update(timestamp + '.' + body)
.digest('hex');
await fetch('https://api.yourera.com/notify/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
'X-Timestamp': timestamp,
'X-Signature': signature,
},
body: body,
});
Send Notification
Send a notification to a patient or internal recipient. The service resolves the appropriate template, renders it with the provided variables, and dispatches to all enabled channels.
Request Body
| Field | Type | Description | |
|---|---|---|---|
type |
string |
required | Notification type. See notification types. |
recipient |
object |
required | Recipient info: { email?: string, phone?: string, name?: string }. At least one of email or phone is required. |
variables |
object |
required | Template variables. Keys match {{variableName}} placeholders in the template. |
channels |
string[] |
optional | Override which channels to use. If omitted, uses the channel config for this type. |
Example Request
{
"type": "prescription_approved",
"recipient": {
"email": "jane@example.com",
"phone": "5041234567",
"name": "Jane Doe"
},
"variables": {
"patientName": "Jane Doe",
"medication": "Semaglutide 5mg/mL",
"pharmacyName": "Galleria Medical Pharmacy"
}
}
Success Response 200
{
"success": true,
"channels": [
{ "channel": "email", "status": "sent" },
{ "channel": "sms", "status": "sent" }
],
"logId": "550e8400-e29b-41d4-a716-446655440000"
}
Error Responses
| Code | Condition | Body |
|---|---|---|
400 |
Unknown notification type | { "error": "Unknown notification type: foo" } |
400 |
Missing required fields | { "error": "Validation failed", "details": [...] } |
400 |
No recipient (email and phone both missing) | { "error": "At least one of email or phone is required" } |
401 |
HMAC auth failure | { "error": "Missing authentication headers" } |
Notification Types
43 notification types organized in 3 categories. Each type is seeded on first deployment with an email template. SMS templates are added as A2P 10DLC registration completes.
General (29 types)
| Type | Description | Required Variables |
|---|---|---|
welcome |
New patient welcome | patientName |
otp |
One-time password for verification | code |
password_reset |
Password reset link | patientName, resetLink |
information_changed |
Profile info was updated | patientName, changedFields |
prescription_changed |
Prescription modification | patientName, medication, change |
what_to_expect |
Post-intake expectations | patientName, intakeType |
doctor_message |
Message from care team | patientName, providerName |
support_message |
Support team message | patientName, message |
first_prescription |
First-ever prescription approved | patientName, medication, providerName, providerSuffix, dosingLine |
new_prescription |
New prescription approved | patientName, medication, pharmacyName |
new_refill |
Refill prescription submitted | patientName, medication |
four_month_checkin |
4-month check-in reminder | patientName |
subscription_renewal |
Subscription auto-renewal notice | patientName, plan, amount, renewalDate |
payment_method_updated |
Card/payment method changed | patientName |
doctor_reviewed |
Doctor completed chart review | patientName, providerName |
magic_link |
Resume intake magic link | magicLinkUrl, intakeType |
follow_up_due |
Follow-up appointment due | patientName, dueDate |
follow_up_past_due |
Follow-up is overdue | patientName, dueDate |
follow_up_two_days |
Follow-up in 2 days | patientName, appointmentDate |
checkin_renewal |
Check-in for subscription renewal | patientName |
visit_scheduled |
Appointment confirmed | patientName, appointmentDate, providerName |
visit_upcoming |
Appointment reminder | patientName, appointmentDate, providerName |
visit_complete |
Visit summary | patientName, providerName |
three_month_checkin |
3-month check-in reminder | patientName |
billing_plan_change |
Plan upgrade/downgrade | patientName, oldPlan, newPlan, amount |
pending_payment |
Payment is due | patientName, amount, dueDate |
treatment_change |
Treatment plan modified | patientName, medication, change |
balance_expiring |
Account balance expiring soon | patientName, balance, expiryDate |
questionnaire_link |
Health questionnaire to complete | patientName, questionnaireUrl |
Orders (12 types)
| Type | Description | Required Variables |
|---|---|---|
first_order_confirmation |
First-ever order placed | patientName, medication, orderNumber |
order_confirmation |
Order placed | patientName, medication, orderNumber |
receipt |
Payment receipt | patientName, amount, orderNumber |
order_edited |
Order was modified | patientName, orderNumber, change |
order_canceled |
Order canceled | patientName, orderNumber, reason |
abandoned_checkout |
Intake started but not finished (30 min) | magicLinkUrl, intakeType |
abandoned_post_checkout |
Payment saved but intake not submitted | patientName, magicLinkUrl |
payment_error |
Payment charge failed | patientName, amount, reason |
lab_received |
Lab sample received | patientName |
lab_resulted |
Lab results ready | patientName |
lab_rejected |
Lab sample rejected | patientName, reason |
treatment_active |
Treatment is now active | patientName, medication |
Shipping (6 types)
| Type | Description | Required Variables |
|---|---|---|
first_shipping_confirmation |
First-ever shipment created | patientName, medication, trackingNumber, carrier |
shipping_confirmation |
Shipment created | patientName, medication, trackingNumber, carrier |
shipping_update |
Shipment status change | patientName, medication, status, trackingNumber, carrier |
delivery_tomorrow |
Package arriving tomorrow | patientName, medication, trackingNumber |
out_for_delivery |
Package out for delivery | patientName, medication, trackingNumber |
delivered |
Package delivered | patientName, medication, trackingNumber |
Channel Matrix
The full channel matrix for all 43 notification types is managed through the Admin Portal.
On initial deployment, all types are seeded with Email enabled and all other
channels disabled. Channels can be toggled per-type via the admin UI or the
PUT /notify/channels/:type API.
Available Channels
| Channel | Provider | Status |
|---|---|---|
| AWS SES | Live | |
| SMS | Twilio | Pending (A2P 10DLC registration) |
| Push | Not implemented | Planned |
| Slack | Slack Webhooks | Internal only |
Template Engine
Templates use Mustache-style {{variableName}} placeholders. The template engine
substitutes variables and handles edge cases gracefully:
- Missing variables render as empty string (no crash)
undefined/nullvariables render as empty string- Variables in email templates are HTML-escaped (XSS prevention)
- Variables in SMS templates are rendered raw (plain text)
- Empty template body causes that channel to be skipped
Example
// Template
"Hi {{patientName}}, your {{medication}} has been approved!"
// Variables
{ "patientName": "Jane", "medication": "Semaglutide" }
// Result
"Hi Jane, your Semaglutide has been approved!"
Templates CRUD
Admin endpoints for managing notification templates. Requires Admin JWT in Authorization: Bearer <token>.
List all notification templates.
Get template for a specific notification type.
Update template for a notification type.
Update Request Body
| Field | Type | Description | |
|---|---|---|---|
emailSubject |
string |
optional | Email subject line template |
emailHtml |
string |
optional | Email HTML body template |
emailText |
string |
optional | Email plain text body template |
smsBody |
string |
optional | SMS message body template |
Channels CRUD
Admin endpoints for managing channel configuration per notification type.
List channel configuration for all notification types.
Update channel configuration for a notification type.
Update Request Body
{
"email": true,
"sms": true,
"hubspot": false,
"slack": false
}
false is allowed. This effectively
disables notifications for that type. The send endpoint will return success with
"channels": [].
Delivery Log
Every notification dispatch is logged with:
| Field | Description |
|---|---|
id | UUID log entry ID |
notificationType | The notification type sent |
recipientEmail | Email address (if sent) |
recipientPhone | Phone number (if sent) |
channels | Array of channel results |
status | "sent", "partial", or "failed" |
createdAt | ISO 8601 timestamp |
Health Check
Health check endpoint. No authentication required.
{
"status": "ok",
"service": "notification-service",
"timestamp": "2026-03-20T12:00:00.000Z"
}
Admin Portal Integration
The admin portal at admin.yourera.com will include a Notifications page
for managing the full notification lifecycle. The page provides:
- Type listing - all 43 types grouped by category (General, Orders, Shipping)
- Channel toggles - enable/disable email, SMS, push per type
- Template editor - edit email subject, HTML body, text body, SMS body for each type
- Delivery log viewer - search/filter notification delivery history
- Test send - send a test notification to verify templates
The admin portal calls the notification service Admin JWT-authenticated endpoints:
| Endpoint | Description |
|---|---|
GET /notify/templates |
Load all templates |
PUT /notify/templates/:type |
Update a template |
GET /notify/channels |
Load channel config |
PUT /notify/channels/:type |
Toggle channels |
GET /notify/log |
Delivery history |
Email Templates & Design System
Overview
All patient-facing emails (except OTP login codes) use the YourEra branded design system. The design was originally created in HubSpot portal 244430850 and is now self-hosted. There are two categories of templates:
- Image-based lifecycle emails - HubSpot-designed, full visual layouts using stacked images (welcome series, new patient, abandoned intake)
- Branded transactional emails - HTML text content wrapped in the branded email shell with logo header + shared footer (prescription approved, shipment, dosing instructions, payment, provider message)
The OTP/login code email uses a minimal standalone layout with no heavy branding.
Image Hosting
All email images are self-hosted on CloudFront rather than the HubSpot CDN.
| Resource | Location |
|---|---|
| CDN URL | https://intake.hisera.com/email-assets/ |
| S3 bucket | hisera-static-assets/email-assets/ |
| Source images | canvas-pioneer-integration-service/email-templates/hubspot-export/images/ |
Assets include template images (JPG/GIF), the YourEra logo, and social media icons (Facebook, Instagram, LinkedIn).
Template Architecture
The template system is built around a shared branded wrapper that provides consistent header, footer, and styling across all transactional emails.
| Module | Description |
|---|---|
email-wrapper.ts |
brandedWrapper(bodyHtml) - wraps any HTML body in the branded shell (logo header, gray background, white content area, shared footer with disclaimer, social links, address, copyright) |
templates.ts |
Template functions that return { subject, html, text } objects. All use brandedWrapper() except loginCode(). |
Helper functions available in templates.ts:
ctaButton(text, href)- renders a styled call-to-action buttoninfoBox(content)- renders a highlighted info boxwarningBox(content)- renders a warning-styled box
Available Templates
Branded Transactional Templates
| Template | Used By | Description |
|---|---|---|
prescriptionApproved() |
Orchestrator | Generic Rx approval notification |
dosingInstructionsEmail() |
Orchestrator | Rx approval with provider details + dosing instructions |
shipmentNotification() |
Shipping service | Tracking number + carrier link |
paymentCharged() |
Payment flow | Payment confirmation receipt |
providerMessage() |
HubSpot fallback | Care team message notification |
welcomeEmail() |
Portal invite | New patient welcome |
OTP Template (Minimal Layout)
| Template | Used By | Description |
|---|---|---|
loginCode() |
Patient portal | Verification code - standalone layout, no branded wrapper |
Image-Based Lifecycle Templates (HubSpot Workflows)
| Template | Workflow | Description |
|---|---|---|
new-patient-post-purchase |
HubSpot - New Patient | "YourEra starts now" onboarding email |
welcome-email-1 |
HubSpot - Welcome Series | Welcome drip series (email 1) |
welcome-email-2 |
HubSpot - Welcome Series | Welcome drip series (email 2) |
welcome-email-3 |
HubSpot - Welcome Series | Welcome drip series (email 3) |
abandoned-intake-1 |
HubSpot - Abandoned Intake | Abandoned intake recovery (email 1) |
abandoned-intake-2 |
HubSpot - Abandoned Intake | Abandoned intake recovery (email 2) |
new-patient-2 |
HubSpot - New Patient | Portal resources email |
Sending Emails
Emails are sent via SendGrid using sendEmail() from
src/lib/notifications/email.ts.
| Environment Variable | Description |
|---|---|
SENDGRID_API_KEY |
SendGrid API key (also in AWS Secrets Manager: hisera/sendgrid) |
NOTIFICATION_FROM_EMAIL |
Sender address (default: YourEra <noreply@yourera.com>) |
Sending Test Emails
Use the test script to send image-based HubSpot templates to a recipient for review:
# Test an image-based HubSpot template
SENDGRID_API_KEY=SG.xxx npx tsx scripts/send-test-email.ts <email> [template-name]
# Available template names:
# abandoned-intake-1, abandoned-intake-2, new-patient-2,
# new-patient-post-purchase, welcome-email-1, welcome-email-2,
# welcome-email-3
DNS Requirements
The yourera.com domain must have proper email authentication records
to ensure deliverability.
| Record | Configuration | Status |
|---|---|---|
| SPF | Must include include:sendgrid.net |
Currently missing - emails may softfail SPF |
| DKIM | s1._domainkey.yourera.com and s2._domainkey.yourera.com pointing to SendGrid |
Configured |
| DMARC | v=DMARC1; p=none; |
Monitoring mode |
include:sendgrid.net to the yourera.com SPF TXT record to resolve this.
Shared Footer Content
All branded emails (those using brandedWrapper()) include the standard footer with:
- FDA compounding pharmacy disclaimer
- YourEra logo
- Privacy Policy + Contact us links
- Social media icons (Facebook, Instagram, LinkedIn)
- Company address: YourEra, 1331 Ochsner Blvd, Suite 200, Covington, LA 70433
- Copyright notice
Roadmap
Phase 1 - Email Only (Current)
- 43 notification types seeded with email templates
- Email delivery via AWS SES (
noreply@yourera.com) - Admin portal can toggle types on/off and edit templates
- SMS channel disabled (pending Twilio A2P 10DLC registration)
Phase 2 - SMS
- Complete Twilio A2P 10DLC registration
- Enable SMS templates for patient-facing notifications
- Admin portal gains SMS template editing
Phase 3 - Intake Magic Link + Abandoned Checkout
magic_linkandabandoned_checkouttypes go live- Intake gateway tracks session start timestamps
- 30-minute inactivity trigger sends abandoned_checkout email
- Magic link includes signed token for intake resume
Phase 4 - Scheduled Notifications
- Follow-up reminders (due, past due, 2 days)
- Check-in reminders (3-month, 4-month)
- Subscription renewal notices
- Delivery estimates (tomorrow, out for delivery)
- Requires a notification scheduler (cron or EventBridge)
Phase 5 - Push Notifications
- Mobile push via APNs/FCM
- In-app notification center
- Admin portal gains push template editing
What's Missing (Dependency Tracker)
| Dependency | Blocks | Status |
|---|---|---|
| AWS SES production access (us-east-1) | All email delivery | Pending |
| Twilio A2P 10DLC | SMS delivery | Pending registration |
| Notification service DB created | Service startup | Pending |
| Admin portal notifications page | Template/channel management | Not started |
| Intake gateway magic link flow | abandoned_checkout, magic_link |
Not started |
| EventBridge scheduler | follow_up_*, checkin_*, delivery_* |
Not started |
| Mobile app push integration | Push notifications | Not started |