Building a Communication Platform: Twilio + SendGrid + Slack
·APIScout Team
communicationtwilioemailslackapi stack
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 |
Compare communication APIs on APIScout — email, SMS, push, and chat providers with pricing calculators and feature comparisons.