diff --git a/api/src/billing/billing-notifications.listener.ts b/api/src/billing/billing-notifications.listener.ts
index c1c1954..e78ed50 100644
--- a/api/src/billing/billing-notifications.listener.ts
+++ b/api/src/billing/billing-notifications.listener.ts
@@ -39,16 +39,26 @@ export class BillingNotificationsListener {
return
}
- const html = this.buildEmailHtml({
- name: user.name?.split(' ')?.[0] || 'there',
- title: payload.title,
- message: payload.message,
- })
+ const subject = this.subjectForType(payload.type, payload.title)
+ const ctaUrlBase = process.env.FRONTEND_URL || 'https://app.textbee.dev'
+ const isEmailVerification = payload.type === 'email_verification_required'
+ const ctaUrl = isEmailVerification
+ ? `${ctaUrlBase}/dashboard/account`
+ : 'https://textbee.dev/#pricing'
+ const ctaLabel = isEmailVerification ? 'Verify your email' : 'View plans & pricing'
- await this.mailService.sendEmail({
+ await this.mailService.sendEmailFromTemplate({
to: user.email,
- subject: payload.title,
- html,
+ subject,
+ template: 'billing-notification',
+ context: {
+ name: user.name?.split(' ')?.[0] || 'there',
+ title: payload.title,
+ message: payload.message,
+ ctaLabel,
+ ctaUrl,
+ brandName: 'textbee.dev',
+ },
from: undefined,
})
@@ -58,20 +68,22 @@ export class BillingNotificationsListener {
)
}
- private buildEmailHtml({
- name,
- title,
- message,
- }: {
- name: string
- title: string
- message: string
- }) {
- return `
-
-
${title}
-
Hi ${name}, ${message}
-
- `
+ private subjectForType(type: string, fallback: string) {
+ switch (type) {
+ case 'daily_limit_reached':
+ return 'Daily SMS limit reached — action required'
+ case 'monthly_limit_reached':
+ return 'Monthly SMS limit reached — action required'
+ case 'bulk_sms_limit_reached':
+ return 'Bulk send limit exceeded'
+ case 'daily_limit_approaching':
+ return 'Heads up: daily usage nearing your limit'
+ case 'monthly_limit_approaching':
+ return 'Heads up: monthly usage nearing your limit'
+ case 'email_verification_required':
+ return 'Verify your email to keep using textbee'
+ default:
+ return fallback || 'Account notification'
+ }
}
}
diff --git a/api/src/billing/billing.service.ts b/api/src/billing/billing.service.ts
index 6a45d6a..af25dab 100644
--- a/api/src/billing/billing.service.ts
+++ b/api/src/billing/billing.service.ts
@@ -85,8 +85,8 @@ export class BillingService {
.notifyOnce({
userId: user._id,
type: BillingNotificationType.DAILY_LIMIT_APPROACHING,
- title: 'Daily limit approaching',
- message: `You have used ${Math.round(dailyPct * 100)}% of your daily limit. ${plan.dailyLimit - processedSmsToday} messages remaining today.`,
+ title: 'You’re nearing today’s SMS limit',
+ message: `You’ve used ${Math.round(dailyPct * 100)}% of today’s SMS allocation. ${plan.dailyLimit - processedSmsToday} messages remain for today. Consider upgrading your plan or scheduling sends for later.`,
meta: { processedSmsToday, dailyLimit: plan.dailyLimit },
sendEmail: true,
})
@@ -100,8 +100,8 @@ export class BillingService {
.notifyOnce({
userId: user._id,
type: BillingNotificationType.MONTHLY_LIMIT_APPROACHING,
- title: 'Monthly limit approaching',
- message: `You have used ${Math.round(monthlyPct * 100)}% of your monthly limit. ${plan.monthlyLimit - processedSmsLastMonth} messages remaining this month.`,
+ title: 'You’re nearing this month’s SMS limit',
+ message: `You’ve used ${Math.round(monthlyPct * 100)}% of this month’s SMS allocation. ${plan.monthlyLimit - processedSmsLastMonth} messages remain this billing period. Upgrade to increase your monthly capacity.`,
meta: { processedSmsLastMonth, monthlyLimit: plan.monthlyLimit },
sendEmail: true,
})
@@ -135,8 +135,8 @@ export class BillingService {
.notifyOnce({
userId: user._id,
type: BillingNotificationType.DAILY_LIMIT_APPROACHING,
- title: 'Daily limit approaching',
- message: `You have used ${Math.round(dailyPct * 100)}% of your daily limit. ${plan.dailyLimit - processedSmsToday} messages remaining today.`,
+ title: 'You’re nearing today’s SMS limit',
+ message: `You’ve used ${Math.round(dailyPct * 100)}% of today’s SMS allocation. ${plan.dailyLimit - processedSmsToday} messages remain for today. Consider upgrading your plan or scheduling sends for later.`,
meta: { processedSmsToday, dailyLimit: plan.dailyLimit },
sendEmail: true,
})
@@ -150,8 +150,8 @@ export class BillingService {
.notifyOnce({
userId: user._id,
type: BillingNotificationType.MONTHLY_LIMIT_APPROACHING,
- title: 'Monthly limit approaching',
- message: `You have used ${Math.round(monthlyPct * 100)}% of your monthly limit. ${plan.monthlyLimit - processedSmsLastMonth} messages remaining this month.`,
+ title: 'You’re nearing this month’s SMS limit',
+ message: `You’ve used ${Math.round(monthlyPct * 100)}% of this month’s SMS allocation. ${plan.monthlyLimit - processedSmsLastMonth} messages remain this billing period. Upgrade to increase your monthly capacity.`,
meta: { processedSmsLastMonth, monthlyLimit: plan.monthlyLimit },
sendEmail: true,
})
@@ -500,17 +500,17 @@ export class BillingService {
if (dailyExceeded) {
hasReachedLimit = true
- message = `Daily limit reached. ${Math.max(0, plan.dailyLimit - processedSmsToday)} remaining today.`
+ message = `Daily SMS limit reached — you’ve used your full daily allocation. ${Math.max(0, plan.dailyLimit - processedSmsToday)} messages remain for today. Upgrade to increase your daily capacity or try again tomorrow.`
}
if (monthlyExceeded) {
hasReachedLimit = true
- message = `Monthly limit reached. ${Math.max(0, plan.monthlyLimit - processedSmsLastMonth)} remaining this month.`
+ message = `Monthly SMS limit reached — you’ve used this billing period’s allocation. Upgrade to continue sending immediately, or wait for the next billing period.`
}
if (bulkExceeded) {
hasReachedLimit = true
- message = `Bulk send limit is ${plan.bulkSendLimit} messages per batch.`
+ message = `Bulk send limit exceeded — your plan allows up to ${plan.bulkSendLimit} messages per batch. Split your send into smaller batches or upgrade your plan.`
}
}
@@ -534,18 +534,22 @@ export class BillingService {
)
// send a deduped notification (single call here for minimal change)
let type: BillingNotificationType
+ let titleForEmail = ''
if (dailyExceeded) {
type = BillingNotificationType.DAILY_LIMIT_REACHED
+ titleForEmail = 'Daily SMS limit reached'
} else if (monthlyExceeded) {
type = BillingNotificationType.MONTHLY_LIMIT_REACHED
+ titleForEmail = 'Monthly SMS limit reached'
} else if (bulkExceeded) {
type = BillingNotificationType.BULK_SMS_LIMIT_REACHED
+ titleForEmail = 'Bulk send limit exceeded'
}
if (type) {
await this.billingNotifications.notifyOnce({
userId: user._id,
type,
- title: message.split('.')[0],
+ title: titleForEmail || 'Usage limit notice',
message,
meta: {
processedSmsToday,
diff --git a/api/src/mail/templates/billing-notification.hbs b/api/src/mail/templates/billing-notification.hbs
new file mode 100644
index 0000000..466cacb
--- /dev/null
+++ b/api/src/mail/templates/billing-notification.hbs
@@ -0,0 +1,87 @@
+
+
+
+
+
+ {{title}} • {{brandName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{brandName}} |
+
+
+ |
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+ {{title}}
+ |
+
+
+ |
+ Hi {{name}},
+ {{message}}
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+ © 2025 {{brandName}}. All rights reserved.
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+