Skip to main content

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

TypeExampleChannelTiming
TransactionalPassword reset, receiptsEmail (required)Immediate
NotificationNew comment, status updateEmail + pushImmediate or batched
MarketingNewsletter, product updatesEmailScheduled
DigestWeekly summaryEmailScheduled (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

ChannelProviderCost Per Message
EmailResend$0.001 (1000 free/month)
EmailSendGrid$0.001-0.003
SMS (US)Twilio$0.0079
SMS (International)Twilio$0.02-0.15
PushFCMFree
PushOneSignalFree (10K MAU)
SlackWebhookFree

Common Mistakes

MistakeImpactFix
No user preferencesUsers unsubscribe or mark as spamLet users choose channels per notification type
Same message all channelsFeels spammyAdapt message format per channel
No rate limitingNotification spamBatch low-priority notifications
No delivery trackingDon't know if messages reach usersTrack delivery status per channel
Critical alerts respect "mute"Users miss important infoOverride preferences for security/payment
No unsubscribe mechanismCAN-SPAM / GDPR violationsEasy unsubscribe in every email

Compare communication APIs on APIScout — email, SMS, push, and chat providers with pricing calculators and feature comparisons.

Comments