Notification Service API

Outbound comms + CRM-lite. Delivery (SES + Twilio), contacts (leads + patients), campaigns (drips + scheduled + blasts), and preferences (HIPAA-aware). Replaces HubSpot entirely.

Canvas-first architecture update (2026-04-21).

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

  1. Caller sends a POST /notify/send with a notification type and recipient data
  2. Service resolves the template for that notification type
  3. Template engine renders the template with provided variables
  4. Channel config determines which channels are enabled for this type
  5. Service dispatches to all enabled channels (email, SMS, HubSpot, Slack)
  6. Each channel delivery is independent - one failure doesn't block others
  7. 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)

HeaderDescription
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

POST /notify/send

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

FieldTypeDescription
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

CodeConditionBody
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)

TypeDescriptionRequired 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)

TypeDescriptionRequired 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)

TypeDescriptionRequired 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

ChannelProviderStatus
EmailAWS SESLive
SMSTwilioPending (A2P 10DLC registration)
PushNot implementedPlanned
SlackSlack WebhooksInternal only

Template Engine

Templates use Mustache-style {{variableName}} placeholders. The template engine substitutes variables and handles edge cases gracefully:

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>.

GET /notify/templates

List all notification templates.

GET /notify/templates/:type

Get template for a specific notification type.

PUT /notify/templates/:type

Update template for a notification type.

Update Request Body

FieldTypeDescription
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.

GET /notify/channels

List channel configuration for all notification types.

PUT /notify/channels/:type

Update channel configuration for a notification type.

Update Request Body

{
  "email": true,
  "sms": true,
  "hubspot": false,
  "slack": false
}
Note: Setting all channels to 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:

FieldDescription
idUUID log entry ID
notificationTypeThe notification type sent
recipientEmailEmail address (if sent)
recipientPhonePhone number (if sent)
channelsArray of channel results
status"sent", "partial", or "failed"
createdAtISO 8601 timestamp

Health Check

GET /health

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:

The admin portal calls the notification service Admin JWT-authenticated endpoints:

EndpointDescription
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:

  1. Image-based lifecycle emails - HubSpot-designed, full visual layouts using stacked images (welcome series, new patient, abandoned intake)
  2. 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.

ResourceLocation
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.

ModuleDescription
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:

Available Templates

Branded Transactional Templates

TemplateUsed ByDescription
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)

TemplateUsed ByDescription
loginCode() Patient portal Verification code - standalone layout, no branded wrapper

Image-Based Lifecycle Templates (HubSpot Workflows)

TemplateWorkflowDescription
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 VariableDescription
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.

RecordConfigurationStatus
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
Note: The missing SPF record means SendGrid emails may softfail SPF checks. Add 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:

Roadmap

Phase 1 - Email Only (Current)

Phase 2 - SMS

Phase 3 - Intake Magic Link + Abandoned Checkout

Phase 4 - Scheduled Notifications

Phase 5 - Push Notifications

What's Missing (Dependency Tracker)

DependencyBlocksStatus
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