Building a Communication Platform 2026
Building a Communication Platform: Twilio + SendGrid + Slack
Modern apps communicate through multiple channels: email for receipts, SMS for 2FA, push for real-time alerts, Slack for team notifications. Here's how to build a unified communication system with the right API for each channel.
The Communication Stack
┌─────────────────────────────────────┐
│ Notification Router │ Route messages to right channel
│ (preferences, priority, rules) │
├─────────────────────────────────────┤
│ Email │ SMS/Voice │ Resend, SendGrid │ Twilio
│ (transactional │ (2FA, alerts, │
│ marketing) │ notifications) │
├─────────────────┼───────────────────┤
│ Push │ In-App / Chat │ FCM, OneSignal │ Stream, Knock
│ (mobile, web) │ (real-time) │
├─────────────────┼───────────────────┤
│ Team Channels │ Webhooks │ Slack, Discord │ Custom
│ (internal) │ (integrations) │
└─────────────────┴───────────────────┘
Channel 1: Email (Resend or SendGrid)
Transactional Email with Resend
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
// Send transactional email
async function sendWelcomeEmail(user: { name: string; email: string }) {
await resend.emails.send({
from: 'Your App <hello@yourapp.com>',
to: user.email,
subject: `Welcome to YourApp, ${user.name}!`,
html: `
<h1>Welcome, ${user.name}!</h1>
<p>Your account is ready. Here's how to get started:</p>
<a href="https://app.yourapp.com/onboarding">Start Onboarding →</a>
`,
});
}
// Send with React Email templates
import { WelcomeEmail } from '@/emails/welcome';
async function sendWelcomeEmailReact(user: { name: string; email: string }) {
await resend.emails.send({
from: 'Your App <hello@yourapp.com>',
to: user.email,
subject: `Welcome to YourApp, ${user.name}!`,
react: WelcomeEmail({ name: user.name }),
});
}
Email Types and When to Send
| Type | Example | Channel | Timing |
|---|---|---|---|
| Transactional | Password reset, receipts | Email (required) | Immediate |
| Notification | New comment, status update | Email + push | Immediate or batched |
| Marketing | Newsletter, product updates | Scheduled | |
| Digest | Weekly summary | Scheduled (weekly) |
Channel 2: SMS with Twilio
import twilio from 'twilio';
const client = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN
);
// Send SMS
async function sendSMS(to: string, body: string) {
return client.messages.create({
body,
to,
from: process.env.TWILIO_PHONE_NUMBER,
});
}
// Send 2FA code
async function send2FACode(phoneNumber: string) {
const code = Math.floor(100000 + Math.random() * 900000).toString();
// Store code with expiration
await db.verificationCodes.create({
phoneNumber,
code,
expiresAt: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes
});
await sendSMS(phoneNumber, `Your verification code is: ${code}. Expires in 10 minutes.`);
return { sent: true };
}
// Or use Twilio Verify (managed 2FA)
async function sendVerification(phoneNumber: string) {
return client.verify.v2
.services(process.env.TWILIO_VERIFY_SID!)
.verifications.create({
to: phoneNumber,
channel: 'sms',
});
}
async function checkVerification(phoneNumber: string, code: string) {
const check = await client.verify.v2
.services(process.env.TWILIO_VERIFY_SID!)
.verificationChecks.create({
to: phoneNumber,
code,
});
return check.status === 'approved';
}
Channel 3: Slack Notifications
// Send notifications to Slack via webhooks
async function sendSlackNotification(
webhookUrl: string,
message: {
title: string;
text: string;
color?: string;
fields?: Array<{ title: string; value: string; short?: boolean }>;
}
) {
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attachments: [{
color: message.color || '#36a64f',
title: message.title,
text: message.text,
fields: message.fields?.map(f => ({
title: f.title,
value: f.value,
short: f.short ?? true,
})),
ts: Math.floor(Date.now() / 1000),
}],
}),
});
}
// Send to Slack via Bot API (more features)
async function sendSlackMessage(channel: string, blocks: any[]) {
await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.SLACK_BOT_TOKEN}`,
},
body: JSON.stringify({ channel, blocks }),
});
}
Channel 4: Push Notifications
// Web Push with Firebase Cloud Messaging (FCM)
import admin from 'firebase-admin';
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
async function sendPushNotification(
deviceToken: string,
notification: { title: string; body: string; url?: string }
) {
await admin.messaging().send({
token: deviceToken,
notification: {
title: notification.title,
body: notification.body,
},
webpush: {
fcmOptions: {
link: notification.url || 'https://app.yourapp.com',
},
},
});
}
// Send to multiple devices
async function sendBulkPush(
tokens: string[],
notification: { title: string; body: string }
) {
// FCM supports up to 500 tokens per batch
const batches = [];
for (let i = 0; i < tokens.length; i += 500) {
batches.push(tokens.slice(i, i + 500));
}
for (const batch of batches) {
await admin.messaging().sendEachForMulticast({
tokens: batch,
notification,
});
}
}
The Unified Notification Router
// Route notifications to the right channel based on type and user preferences
interface NotificationPayload {
userId: string;
type: 'order_confirmed' | 'new_message' | 'payment_failed' | 'security_alert' | 'weekly_digest';
title: string;
body: string;
data?: Record<string, string>;
}
// Channel routing rules
const NOTIFICATION_RULES: Record<string, {
channels: ('email' | 'sms' | 'push' | 'slack')[];
priority: 'high' | 'normal' | 'low';
respectPreferences: boolean;
}> = {
order_confirmed: {
channels: ['email', 'push'],
priority: 'normal',
respectPreferences: true,
},
new_message: {
channels: ['push'],
priority: 'normal',
respectPreferences: true,
},
payment_failed: {
channels: ['email', 'sms', 'push'],
priority: 'high',
respectPreferences: false, // Always send critical notifications
},
security_alert: {
channels: ['email', 'sms'],
priority: 'high',
respectPreferences: false,
},
weekly_digest: {
channels: ['email'],
priority: 'low',
respectPreferences: true,
},
};
class NotificationRouter {
async send(notification: NotificationPayload) {
const rules = NOTIFICATION_RULES[notification.type];
if (!rules) throw new Error(`Unknown notification type: ${notification.type}`);
const user = await db.users.findById(notification.userId);
const preferences = await db.notificationPreferences.findByUser(notification.userId);
const channels = rules.channels.filter(channel => {
if (!rules.respectPreferences) return true;
return preferences[channel] !== false;
});
// Send to each channel in parallel
const results = await Promise.allSettled(
channels.map(channel =>
this.sendToChannel(channel, user, notification)
)
);
// Log results
for (const [i, result] of results.entries()) {
await db.notificationLog.create({
userId: notification.userId,
type: notification.type,
channel: channels[i],
status: result.status === 'fulfilled' ? 'sent' : 'failed',
error: result.status === 'rejected' ? String(result.reason) : undefined,
});
}
}
private async sendToChannel(
channel: string,
user: User,
notification: NotificationPayload
) {
switch (channel) {
case 'email':
return sendEmail(user.email, notification.title, notification.body);
case 'sms':
return sendSMS(user.phone, `${notification.title}: ${notification.body}`);
case 'push':
const tokens = await db.pushTokens.findByUser(user.id);
return Promise.all(tokens.map(t =>
sendPushNotification(t.token, notification)
));
case 'slack':
return sendSlackNotification(SLACK_WEBHOOK, {
title: notification.title,
text: notification.body,
});
}
}
}
// Usage
const router = new NotificationRouter();
await router.send({
userId: 'user_123',
type: 'payment_failed',
title: 'Payment Failed',
body: 'Your card ending in 4242 was declined. Please update your payment method.',
});
API Costs Comparison
| Channel | Provider | Cost Per Message |
|---|---|---|
| Resend | $0.001 (1000 free/month) | |
| SendGrid | $0.001-0.003 | |
| SMS (US) | Twilio | $0.0079 |
| SMS (International) | Twilio | $0.02-0.15 |
| Push | FCM | Free |
| Push | OneSignal | Free (10K MAU) |
| Slack | Webhook | Free |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| No user preferences | Users unsubscribe or mark as spam | Let users choose channels per notification type |
| Same message all channels | Feels spammy | Adapt message format per channel |
| No rate limiting | Notification spam | Batch low-priority notifications |
| No delivery tracking | Don't know if messages reach users | Track delivery status per channel |
| Critical alerts respect "mute" | Users miss important info | Override preferences for security/payment |
| No unsubscribe mechanism | CAN-SPAM / GDPR violations | Easy unsubscribe in every email |
Handling Delivery Failures and Fallbacks
Notifications fail. SMS messages hit carrier filters. Emails bounce. Push tokens expire when users uninstall apps. A production communication system needs explicit failure handling and fallback chains rather than optimistic fire-and-forget delivery.
Email delivery failures arrive via webhook. Both Resend and SendGrid post events to a URL you configure: delivered, bounce, spam_report, and unsubscribe. Hard bounces (invalid address, domain doesn't exist) and spam reports must result in immediate suppression — sending to a bounced address again will damage your domain reputation with ISPs. Soft bounces (recipient inbox full, server temporarily unavailable) can be retried after a delay. Track bounce counts per recipient: three soft bounces in 30 days should trigger a suppression, not just retries.
SMS delivery failures from Twilio include carrier-level error codes: 30003 (unreachable destination handset), 30004 (carrier blocked), 30006 (landline or unroutable number), and 30008 (unknown error, retry eligible). When a phone number returns 30006, mark it as a landline in your database and stop sending SMS to it entirely — it will never succeed. Implement a webhook listener at your Twilio StatusCallback URL:
app.post('/webhooks/twilio/status', express.urlencoded({ extended: false }), async (req, res) => {
const { MessageSid, MessageStatus, ErrorCode, To } = req.body;
await db.smsLogs.update({
where: { sid: MessageSid },
data: { status: MessageStatus, errorCode: ErrorCode },
});
if (ErrorCode === '30006') {
// Permanent failure — suppress this number
await db.users.updateMany({
where: { phone: To },
data: { smsEnabled: false, smsDisabledReason: 'landline' },
});
}
res.sendStatus(200);
});
Push token expiration is common — users reinstall apps, change devices, or revoke permissions. FCM returns 404 or UNREGISTERED error codes for expired tokens. Clean up these tokens promptly:
const results = await admin.messaging().sendEachForMulticast({
tokens: batch,
notification,
});
results.responses.forEach((resp, i) => {
if (!resp.success && resp.error?.code === 'messaging/registration-token-not-registered') {
// This token is invalid — delete it:
db.pushTokens.delete({ where: { token: batch[i] } });
}
});
Fallback chains ensure critical notifications reach users even when a preferred channel fails. For high-priority notifications like payment failures or security alerts, implement a cascade: attempt push notification first (instant, free), fall back to SMS if the push fails or the token is missing, fall back to email if SMS is unavailable. The Promise.allSettled pattern in the router above captures failures — extend it to trigger fallbacks:
// After sending push, check if it succeeded; if not, send SMS fallback:
const pushResult = await sendPushNotification(token, notification).catch(() => null);
if (!pushResult && user.phone && notification.priority === 'high') {
await sendSMS(user.phone, `${notification.title}: ${notification.body}`);
}
Email Authentication: SPF, DKIM, and DMARC
Without proper email authentication, your transactional emails — password resets, receipts, account alerts — will land in spam. Gmail, Outlook, and Apple Mail all enforce sender authentication checks, and failing them silently drops delivery rates without generating any bounce event.
SPF (Sender Policy Framework) is a DNS TXT record that lists the IP addresses and domains authorized to send email on behalf of your domain. When you use Resend or SendGrid, they provide the exact SPF record to add to your domain's DNS:
# DNS TXT record for your sending domain:
v=spf1 include:amazonses.com include:sendgrid.net ~all
# For Resend:
v=spf1 include:amazonses.com ~all
DKIM (DomainKeys Identified Mail) adds a cryptographic signature to outgoing emails that receiving servers verify against a public key in your DNS. Both Resend and SendGrid handle the signing automatically — you only need to add CNAME records they provide to your domain's DNS. The setup takes 15 minutes and immediately improves deliverability.
DMARC (Domain-based Message Authentication, Reporting, and Conformance) is the policy that ties SPF and DKIM together and tells receiving mail servers what to do when checks fail. Start with monitoring mode to collect reports before enforcing:
# Phase 1 — Monitor only (add to DNS as TXT record):
v=DMARC1; p=none; rua=mailto:dmarc@yourapp.com; ruf=mailto:dmarc-forensics@yourapp.com
# Phase 2 — Quarantine failing messages (after reviewing reports):
v=DMARC1; p=quarantine; pct=50; rua=mailto:dmarc@yourapp.com
# Phase 3 — Reject failing messages (full enforcement):
v=DMARC1; p=reject; rua=mailto:dmarc@yourapp.com
Spend two weeks in Phase 1 analyzing the DMARC reports from rua before moving to Phase 2. The reports reveal any legitimate sending sources you haven't whitelisted in SPF — internal tools, third-party services, legacy scripts — that would break if you moved to enforcement prematurely. Services like Postmark's DMARC Digests or Valimail parse these XML reports into readable summaries.
Testing before you launch: run your emails through mail-tester.com and MXToolbox's Email Health Check before sending to real users. A score below 8/10 on mail-tester almost always indicates missing DKIM or a misconfigured SPF record.
Rate Limiting, Opt-In, and Legal Compliance
High-volume messaging without compliance controls creates legal liability and deliverability damage. The regulations differ by channel and geography.
SMS marketing in the US is governed by the TCPA (Telephone Consumer Protection Act), which requires prior express written consent before sending any marketing text message. The penalties are up to $500 per message, trebled to $1,500 for willful violations. For transactional SMS (2FA codes, order confirmations, appointment reminders with no marketing content), explicit opt-in is typically not required, but you must honor STOP requests immediately and confirm with "You've been unsubscribed." Twilio's Messaging Services provide automatic STOP/HELP handling; use them rather than building your own keyword processing.
Email marketing in the US falls under CAN-SPAM, which requires a clear unsubscribe mechanism, your physical mailing address in every email, and honoring unsubscribe requests within 10 business days. The EU's GDPR requires documented consent for marketing emails, the right to erasure, and specific data handling agreements with email processors. CASL in Canada is stricter than CAN-SPAM and requires express opt-in for commercial messages. Both Resend and SendGrid maintain suppression lists automatically — when a user unsubscribes, the provider adds them to a global suppression list that prevents future sends regardless of which part of your code triggers them.
Preference centers are an underrated tool for reducing opt-outs. Instead of a single global unsubscribe button, offer channel-specific controls: let users disable marketing emails while keeping transactional emails enabled, or pause SMS notifications while keeping push notifications active. Storing preferences at the notification type level (not just the channel level) reduces total unsubscribes because users can silence the specific notifications they find annoying without opting out of everything.
Rate limiting your own sends prevents ISP throttling. Sending 100K emails in a burst to cold subscribers is a fast path to spam folder placement. Warm up new sending domains over 2-3 weeks: start at 500 emails/day, double every 3 days, monitor your bounce and spam complaint rates before scaling. Both Resend and SendGrid provide sending schedules and volume caps for domain warmup. For SMS, Twilio applies carrier-level rate limits — standard US long codes are limited to approximately 1 message/second; purchase a Messaging Service with multiple phone numbers to increase throughput, or use a 10DLC registered number for A2P business messaging.
Methodology
The unified notification router pattern shown here is a starting point — at scale, consider a dedicated notification service (Knock, Courier, or Novu) that manages preferences, templates, batching, and delivery tracking across channels as a managed product. Building this infrastructure yourself is worthwhile when you need deep custom logic, but the managed services handle common edge cases (unsubscribe propagation, bounce suppression, digest batching) out of the box and can save months of engineering time.
Twilio error codes and SMS carrier behavior sourced from Twilio's official error code documentation and Programmable Messaging documentation as of March 2026. Email authentication (SPF, DKIM, DMARC) guidance follows RFC 7208 (SPF), RFC 6376 (DKIM), and RFC 7489 (DMARC); specific DNS record examples are illustrative — use the exact records provided by your email provider during domain setup. TCPA compliance summary is not legal advice; consult qualified counsel for your specific use case. Pricing figures sourced from Resend, SendGrid, Twilio, and Firebase Cloud Messaging pricing pages as of March 2026. FCM error code handling based on Firebase Admin SDK documentation for Node.js v12.x. Push token cleanup behavior verified from Firebase messaging documentation. For Apple Push Notification service (APNs) token handling, use the APNs error response BadDeviceToken as the equivalent of FCM's UNREGISTERED — both indicate an expired or unregistered device token that should be removed from your database.
Compare communication APIs on APIScout — email, SMS, push, and chat providers with pricing calculators and feature comparisons.
Compare Twilio and Slack on APIScout.
Related: Building a SaaS Backend, How to Integrate Twilio SMS in Any Web App, OpenAI Realtime API: Building Voice Applications 2026