messaging channels
messaging channels
RedwoodJS Two-Way SMS with MessageBird: Inbound & Outbound Messaging Tutorial
Learn how to build production-ready two-way SMS messaging in RedwoodJS with MessageBird webhooks. Complete guide covering SMS sending, receiving inbound messages, webhook security, signature verification, and deployment for real-time SMS communication.
Build Two-Way SMS Messaging in RedwoodJS with MessageBird
Build production-ready two-way SMS messaging in your RedwoodJS application using MessageBird's SMS API and webhook system. This tutorial shows you how to send outbound SMS messages via GraphQL and receive inbound SMS through secure webhooks—perfect for customer support chat, appointment confirmations, OTP verification, automated responses, and real-time notifications.
You'll implement complete bidirectional SMS communication with webhook signature verification, message logging, replay attack prevention, and production deployment strategies for RedwoodJS applications.
What You'll Build: Two-Way SMS Communication System
Core Features:
- Outbound SMS Sending: Trigger SMS messages programmatically through GraphQL mutations from your RedwoodJS application
- Inbound SMS Reception: Handle incoming SMS messages sent to your MessageBird virtual number using secure webhook endpoints
- Message Persistence: Store complete conversation history of both inbound and outbound messages in your database
- Webhook Security: Implement HMAC-SHA256 signature verification and timestamp validation to prevent unauthorized access
Real-World Use Cases:
This two-way SMS system enables interactive communication for appointment reminders with confirmations, customer support conversations, two-factor authentication codes, order status updates with customer replies, survey responses, and automated chatbot interactions.
Technology Stack:
- RedwoodJS v8.0+: Full-stack JavaScript/TypeScript framework with GraphQL and serverless functions. Requires Node.js v20.x or later
- Node.js v20.x+: Runtime environment for the RedwoodJS API side (official source)
- Yarn v1.22.21+: Package manager required by RedwoodJS
- MessageBird SMS API: Communication platform providing SMS API and virtual phone numbers for two-way messaging
- MessageBird Node.js SDK v3.8.0+: Simplifies interaction with the MessageBird API. Requires Node.js >=0.10 (npm package)
- Prisma ORM: Database toolkit used by RedwoodJS for type-safe database access
- Database: PostgreSQL, MySQL, or SQLite (PostgreSQL recommended for production)
- ngrok v3.0+: Development tool to expose your local server for testing inbound SMS webhooks
System Architecture:
_____________________ ________________________ ____________________
| RedwoodJS Web App |----->| RedwoodJS API (GraphQL)|----->| Database |
| (Frontend) | | (Node.js / Prisma) | | (PostgreSQL) |
|___________________| |________________________| |____________________|
^ | | ^ |
| | | |__________________________| (Log Messages)
| (GraphQL Mutation) | | (MessageBird SDK)
| | |
___________________________ |_______________| ________________________
| User's Mobile Device |<----->| MessageBird |<----->| RedwoodJS Function |
| (Sends/Receives SMS) | | (API/Webhook) | | (Webhook Endpoint) |
|___________________________| |_______________| |________________________|
| |
| | (Webhook Config in Flow Builder)
| |
_____________
| `ngrok` | (Development Only)
|___________|Prerequisites:
- Node.js v20.x or later: Download from nodejs.org. Verify installation:
node --version - Yarn v1.22.21 or later: Install via npm:
npm install -g yarn. Verify:yarn --version - RedwoodJS CLI: Install globally:
yarn global add @redwoodjs/cli - MessageBird Account: Sign up at messagebird.com
- Virtual Phone Number: Purchase an SMS-enabled number from MessageBird Dashboard → Numbers → Buy a number for receiving inbound SMS
- ngrok: Download from ngrok.com/download. Free account required for webhook testing
- Database: PostgreSQL recommended. Install locally or use hosted service (e.g., Railway, Render, Supabase)
Expected Outcome:
A fully functional RedwoodJS application that sends SMS via GraphQL and receives/processes/logs incoming SMS messages via secure webhooks for true two-way communication.
1. Setting up the Project
Initialize a new RedwoodJS project, set up the database, and install dependencies.
Step 1.1: Install Prerequisites
Node.js and Yarn:
# Verify Node.js version (must be v20.x or later)
node --version
# If needed, download from https://nodejs.org/
# Install Yarn globally
npm install -g yarn
# Verify Yarn installation
yarn --versionDatabase Setup (PostgreSQL recommended):
For local development, install PostgreSQL:
- macOS:
brew install postgresql@14 && brew services start postgresql@14 - Ubuntu/Debian:
sudo apt-get install postgresql postgresql-contrib - Windows: Download installer from postgresql.org
Or use a hosted database service like Railway, Render, or Supabase.
Step 1.2: Create RedwoodJS Project
Open your terminal and run:
yarn create redwood-app ./redwood-messagebird-app --typescript
cd redwood-messagebird-appUsing TypeScript provides better type safety and developer experience, especially for larger applications.
Step 1.3: Configure Database Connection
Update your .env file with your database connection string:
# .env
DATABASE_URL="postgresql://user:password@localhost:5432/messagebird_dev"
# Or for SQLite (development only):
# DATABASE_URL="file:./dev.db"Run initial database migration:
yarn rw prisma migrate devStep 1.4: Install MessageBird SDK
Navigate to the api directory and add the MessageBird Node.js SDK:
cd api
yarn add messagebird
cd ..SDK version: The messagebird package (v3.8.0+) is compatible with Node.js >=0.10, including Node.js v20.x.
Step 1.5: Set Up MessageBird Account
Obtain API Keys:
- Log in to your MessageBird Dashboard at dashboard.messagebird.com
- Navigate to Developer Settings → API access
- Copy your Live API Key (starts with
live_) for production use - Copy your Test API Key (starts with
test_) for development
Test keys simulate API requests without sending actual SMS or consuming credits. Live keys send real messages and deduct from your balance (official documentation).
Obtain Signing Key:
- In Dashboard, go to Developer Settings → Webhook Signing
- Click Generate signing key
- Copy the signing key (used to verify webhook authenticity)
Purchase Virtual Number:
- Go to Numbers → Buy a number
- Select your country and ensure SMS capability is enabled
- Choose and purchase a number
- Note the number in E.164 format (e.g.,
+14155552671)
Step 1.6: Configure Environment Variables
Never commit API keys to version control. Use environment variables.
Create a .env file in the project root (redwood-messagebird-app/.env):
# .env
# MessageBird API Keys
MESSAGEBIRD_ACCESS_KEY=YOUR_LIVE_OR_TEST_ACCESS_KEY
MESSAGEBIRD_SIGNING_KEY=YOUR_WEBHOOK_SIGNING_KEY
# Your purchased MessageBird number (E.164 format)
MESSAGEBIRD_ORIGINATOR_NUMBER=+14155552671
# (Optional) Your test mobile number (E.164 format)
TEST_RECIPIENT_NUMBER=+12125551234
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/messagebird_dev"E.164 Format: Phone numbers must be in international format without spaces or special characters:
- US number:
+12145551234(country code +1, area code 214, number 5551234) - UK number:
+442012345678(country code +44, area code 20, number 12345678) - Format:
+[country code][area code][local number](max 15 digits) (official documentation)
Add .env to .gitignore:
Ensure .gitignore contains .env to prevent committing keys. RedwoodJS includes this by default.
Step 1.7: Accessing Environment Variables
RedwoodJS automatically loads variables from .env into process.env. Access them in your API-side code (services, functions) like process.env.MESSAGEBIRD_ACCESS_KEY.
2. Implementing Core Functionality (Outbound SMS)
Create the service for sending SMS messages.
Step 2.1: Create the Outbound SMS Service
Generate a new service to encapsulate the outbound SMS logic:
yarn rw g service smsThis creates api/src/services/sms/sms.ts and api/src/services/sms/sms.test.ts.
RedwoodJS services contain business logic and data access. GraphQL resolvers call them, and you can reuse them throughout your application (RedwoodJS documentation).
Configure the sms.ts service:
// api/src/services/sms/sms.ts
import messagebird from 'messagebird';
// Initialize the MessageBird client using API key from environment variable
const client = messagebird(process.env.MESSAGEBIRD_ACCESS_KEY);
/**
* Send an SMS message via MessageBird
* @param {string} to - Recipient phone number in E.164 format (e.g., +12345678901)
* @param {string} body - Message content (160 chars for GSM-7, 70 for Unicode)
* @param {string} originator - Sender ID (phone number or alphanumeric, max 11 chars)
* @returns {Promise<object>} - MessageBird API response with message ID and status
*/
export const sendSMS = ({ to, body, originator }) => {
return new Promise((resolve, reject) => {
// Validate required parameters
if (!to || !body || !originator) {
return reject(new Error('Missing required parameters: to, body, and originator are required'));
}
// Validate E.164 format (basic check)
const e164Regex = /^\+[1-9]\d{1,14}$/;
if (!e164Regex.test(to)) {
return reject(new Error('Recipient number must be in E.164 format (e.g., +12125551234)'));
}
// Check message length and warn about concatenation
const isUnicode = /[^\x00-\x7F]/.test(body);
const maxLength = isUnicode ? 70 : 160;
const partCount = Math.ceil(body.length / maxLength);
if (partCount > 1) {
console.warn(`Message will be split into ${partCount} parts. Cost will be ${partCount}x per message.`);
}
const params = {
originator,
recipients: [to],
body
};
client.messages.create(params, (err, response) => {
if (err) {
console.error('MessageBird API Error:', err);
// Common error types
if (err.errors) {
const errorCodes = err.errors.map(e => `${e.code}: ${e.description}`).join(', ');
return reject(new Error(`MessageBird API Error: ${errorCodes}`));
}
return reject(err);
}
console.log('SMS sent successfully:', response.id);
resolve(response);
});
});
};SMS Character Limits:
- GSM-7 encoding: 160 characters per SMS, up to 1377 characters per concatenated message
- Unicode encoding: 70 characters per SMS, up to 603 characters per concatenated message
- Concatenated messages are billed as multiple SMS (official documentation)
Common API Errors:
- Code 2: Incorrect access key (authentication failed)
- Code 9: Missing required parameters
- Code 21: Insufficient balance
- Code 25: Invalid phone number format
3. Building the API Layer (GraphQL Mutation)
Create a GraphQL mutation to send SMS messages from your web application.
Step 3.1: Define GraphQL Schema
Create or update api/src/graphql/sms.sdl.ts:
// api/src/graphql/sms.sdl.ts
export const schema = gql`
type SMSResponse {
success: Boolean!
messageId: String
error: String
}
type Mutation {
sendSMS(
to: String!
body: String!
originator: String!
): SMSResponse! @requireAuth
}
`;@requireAuth ensures only authenticated users can send SMS. Remove it if you don't need authentication.
Step 3.2: Implement GraphQL Resolver
Update api/src/services/sms/sms.ts to add the resolver:
// Add to api/src/services/sms/sms.ts
interface SendSMSArgs {
to: string;
body: string;
originator: string;
}
export const sendSMS = async ({ to, body, originator }: SendSMSArgs) => {
try {
const response = await new Promise((resolve, reject) => {
// Validate parameters
if (!to || !body || !originator) {
return reject(new Error('Missing required parameters'));
}
const e164Regex = /^\+[1-9]\d{1,14}$/;
if (!e164Regex.test(to)) {
return reject(new Error('Recipient must be in E.164 format'));
}
const params = {
originator,
recipients: [to],
body
};
client.messages.create(params, (err, response) => {
if (err) {
console.error('MessageBird API Error:', err);
return reject(err);
}
resolve(response);
});
});
return {
success: true,
messageId: response.id,
error: null
};
} catch (error) {
console.error('SMS sending failed:', error);
return {
success: false,
messageId: null,
error: error.message
};
}
};4. Building the Webhook Handler (Inbound SMS)
Receive and process incoming SMS messages through a webhook endpoint—the key component for two-way SMS functionality.
Step 4.1: Create the Webhook Handler Function
Generate a serverless function to handle incoming webhooks:
yarn rw g function webhookThis creates api/src/functions/webhook/webhook.ts.
RedwoodJS serverless functions are independent API endpoints ideal for webhooks, background jobs, and external integrations (RedwoodJS documentation).
Implement the webhook handler:
// api/src/functions/webhook/webhook.ts
import crypto from 'crypto';
import { db } from 'src/lib/db';
import type { APIGatewayEvent, Context } from 'aws-lambda';
/**
* Verify MessageBird webhook signature with timestamp validation
* Prevents unauthorized requests and replay attacks
*/
const verifySignature = (event: APIGatewayEvent, body: string): boolean => {
const signature = event.headers['messagebird-signature'];
const timestamp = event.headers['messagebird-request-timestamp'];
if (!signature || !timestamp) {
console.error('Missing signature or timestamp headers');
return false;
}
// Validate timestamp to prevent replay attacks
// Accept requests within 5 minutes (300 seconds) of creation
const requestTime = parseInt(timestamp, 10) * 1000; // Convert to milliseconds
const currentTime = Date.now();
const timeDiff = Math.abs(currentTime - requestTime);
const TOLERANCE_MS = 5 * 60 * 1000; // 5 minutes
if (timeDiff > TOLERANCE_MS) {
console.error(`Timestamp outside tolerance: ${timeDiff}ms difference`);
return false;
}
// Construct the signing payload
const payload = `${timestamp}\n${body}`;
// Calculate expected signature using your signing key
const expectedSignature = crypto
.createHmac('sha256', process.env.MESSAGEBIRD_SIGNING_KEY)
.update(payload)
.digest('hex');
// Compare signatures using timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (err) {
console.error('Signature comparison failed:', err);
return false;
}
};
/**
* Handler for incoming webhook requests from MessageBird
*/
export const handler = async (event: APIGatewayEvent, context: Context) => {
// Only accept POST requests
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' })
};
}
try {
const body = event.body;
// Verify the webhook signature
if (!verifySignature(event, body)) {
console.error('Invalid webhook signature');
return {
statusCode: 401,
body: JSON.stringify({ error: 'Unauthorized' })
};
}
const payload = JSON.parse(body);
// Extract message data from webhook payload
// Webhook payload structure:
// { id, originator, body, recipient, createdDatetime, type, direction }
const {
id,
originator,
body: messageBody,
recipient,
createdDatetime
} = payload;
// Check for duplicate webhook delivery (idempotency)
const existingMessage = await db.message.findUnique({
where: { messageBirdId: id }
});
if (existingMessage) {
console.log('Duplicate webhook ignored:', id);
return {
statusCode: 200,
body: JSON.stringify({ success: true, duplicate: true })
};
}
// Store the incoming message in database
await db.message.create({
data: {
messageBirdId: id,
sender: originator,
recipient: recipient,
body: messageBody,
direction: 'inbound',
receivedAt: new Date(createdDatetime)
}
});
console.log('Inbound message processed:', id);
// Return 200 within 30 seconds to acknowledge receipt
// MessageBird will retry up to 10 times if not acknowledged
return {
statusCode: 200,
body: JSON.stringify({ success: true })
};
} catch (error) {
console.error('Webhook processing error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' })
};
}
};MessageBird Webhook Payload Structure:
Inbound SMS webhooks contain the following fields (official documentation):
id: Unique message identifieroriginator: Sender's phone number (E.164 format)recipient: Your MessageBird virtual numberbody: Message contentcreatedDatetime: Timestamp in RFC3339 formattype: Message type (usually "sms")direction: "mo" (mobile originated = inbound)
Webhook Retry Behavior:
MessageBird retries failed webhooks up to 10 times with exponential backoff. Return HTTP 200 within 30 seconds to acknowledge receipt (webhook documentation).
5. Securing Your Implementation
Protect your webhook endpoint from unauthorized access with signature verification.
Webhook Signature Verification
Verify every webhook request using MessageBird's signing key to prevent unauthorized requests and confirm message authenticity.
How signature verification works:
- MessageBird signs each webhook request using your signing key with HMAC-SHA256
- The signature arrives in the
messagebird-signatureheader - The timestamp arrives in the
messagebird-request-timestampheader (Unix timestamp) - Calculate the expected signature:
HMAC-SHA256(signing_key, timestamp + "\n" + body) - Compare signatures using timing-safe comparison to prevent timing attacks
Timestamp Validation (Prevents Replay Attacks):
Validate the timestamp to prevent replay attacks. Accept requests within a 5-minute tolerance window (security best practices):
const requestTime = parseInt(timestamp, 10) * 1000;
const currentTime = Date.now();
const timeDiff = Math.abs(currentTime - requestTime);
const TOLERANCE_MS = 5 * 60 * 1000; // 5 minutes
if (timeDiff > TOLERANCE_MS) {
return false; // Reject expired requests
}Best practices:
- Reject requests with invalid signatures (return 401 Unauthorized)
- Validate timestamp within 5-minute tolerance
- Log signature verification failures
- Rotate signing keys periodically
- Store signing keys in environment variables
- Use timing-safe comparison (
crypto.timingSafeEqual) - Check for duplicate message IDs before processing
Environment Variable Security
Protect sensitive credentials by storing them as environment variables:
# .env
MESSAGEBIRD_ACCESS_KEY=your_api_key_here
MESSAGEBIRD_SIGNING_KEY=your_signing_key_here
MESSAGEBIRD_ORIGINATOR_NUMBER=+12345678901Security checklist:
- Add
.envto.gitignore - Use different keys for development, staging, and production
- Rotate API keys quarterly or after security incidents
- Monitor API key usage for unusual activity
- Implement rate limiting on webhook endpoints
- Use HTTPS for webhook URLs (required by MessageBird)
- Validate and sanitize incoming data before database insertion
6. Database Schema and Message Logging
Store incoming and outgoing SMS for tracking, analytics, and conversation history.
Step 6.1: Create the Prisma Schema
Define a Message model in your Prisma schema to store SMS data:
// api/db/schema.prisma
model Message {
id String @id @default(cuid())
messageBirdId String @unique // MessageBird's message ID (for idempotency)
sender String // Phone number in E.164 format
recipient String // Phone number in E.164 format
body String // Message content
direction String // "inbound" or "outbound"
status String? // Delivery status (for outbound messages)
receivedAt DateTime @default(now())
sentAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Indexes for common queries
@@index([sender])
@@index([recipient])
@@index([direction])
@@index([receivedAt])
}Schema improvements:
@@uniqueconstraint onmessageBirdIdprevents duplicate webhook processing- Indexes on
sender,recipient,direction, andreceivedAtoptimize common queries statusfield stores delivery status for outbound messages (sent, delivered, failed)
Step 6.2: Apply Database Migrations
Run Prisma migrations to create the database table:
yarn rw prisma migrate dev --name add_message_modelThis:
- Creates a migration file in
api/db/migrations/ - Applies the migration to your development database
- Regenerates Prisma Client with the new model
Migration conflicts: Use yarn rw prisma migrate reset to reset the database. This deletes all data in development.
Step 6.3: Query Messages
Use Prisma Client to query message history:
// Get all messages for a specific phone number
const messages = await db.message.findMany({
where: {
OR: [
{ sender: '+12345678901' },
{ recipient: '+12345678901' }
]
},
orderBy: {
receivedAt: 'desc'
}
});
// Get conversation between two numbers
const conversation = await db.message.findMany({
where: {
OR: [
{ sender: '+12345678901', recipient: '+10987654321' },
{ sender: '+10987654321', recipient: '+12345678901' }
]
},
orderBy: {
receivedAt: 'asc'
}
});
// Pagination for large message volumes
const PAGE_SIZE = 50;
const page = 1;
const paginatedMessages = await db.message.findMany({
take: PAGE_SIZE,
skip: (page - 1) * PAGE_SIZE,
orderBy: { receivedAt: 'desc' }
});7. Testing Your Implementation
Test your SMS integration before deploying to production.
Testing Outbound SMS
Test SMS sending through your GraphQL API:
mutation SendTestSMS {
sendSMS(
to: "+12125551234"
body: "Test message from RedwoodJS"
originator: "+14155552671"
) {
success
messageId
error
}
}Verification steps:
- Check your phone for the received SMS
- Verify the message appears in MessageBird dashboard (Messages section)
- Confirm the message saves to your database:
yarn rw prisma studio - Check API logs for any errors:
yarn rw dev
Testing with Test Keys:
Use MessageBird test keys (test_xxxxxxx) for development. Test keys simulate API calls without sending SMS or consuming credits. The API returns success responses, but delivers no messages (official documentation).
Testing Inbound Webhooks Locally
Use ngrok to expose your local development server for webhook testing:
Step 1: Install and start ngrok:
# Install ngrok
# macOS: brew install ngrok
# Linux: snap install ngrok
# Windows: Download from https://ngrok.com/download
# Authenticate (required for ngrok v3+)
ngrok config add-authtoken YOUR_NGROK_AUTH_TOKEN
# Start ngrok tunnel on RedwoodJS API port (default 8911)
ngrok http 8911Step 2: Configure MessageBird webhook:
- Copy the HTTPS URL from ngrok (e.g.,
https://abc123.ngrok.io) - Go to MessageBird Dashboard → Flow Builder
- Create a new flow or select existing flow
- Select your virtual number as the trigger
- Add "Call HTTP endpoint with SMS" step
- Set webhook URL to:
https://abc123.ngrok.io/.redwood/functions/webhook - Method: POST
- Click Publish to activate
Step 3: Send test SMS:
Send an SMS from your phone to your MessageBird number. Monitor:
- ngrok console for incoming requests (shows headers and payload)
- RedwoodJS logs (
yarn rw dev) for webhook processing - Database (Prisma Studio:
yarn rw prisma studio) for stored message records
Simulating Webhook Requests
Test webhook handling without sending actual SMS using curl with a valid signature:
Calculate valid test signature:
# 1. Get current Unix timestamp
TIMESTAMP=$(date +%s)
# 2. Create payload file
cat > payload.json << EOF
{
"id": "test-msg-12345",
"originator": "+12125551234",
"body": "Test inbound message",
"recipient": "+14155552671",
"createdDatetime": "2024-01-15T10:30:00Z"
}
EOF
# 3. Calculate signature
PAYLOAD=$(cat payload.json)
SIGNING_KEY="your_signing_key_here"
SIGNATURE=$(echo -n "${TIMESTAMP}\n${PAYLOAD}" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | awk '{print $2}')
# 4. Send request with valid signature
curl -X POST http://localhost:8911/.redwood/functions/webhook \
-H "Content-Type: application/json" \
-H "messagebird-signature: $SIGNATURE" \
-H "messagebird-request-timestamp: $TIMESTAMP" \
-d @payload.jsonCommon Testing Issues
Problem: Webhook receives requests but returns 401
- Verify your signing key matches the MessageBird dashboard value
- Check timestamp format in signature calculation
- Ensure signing key is set in
.env
Problem: Messages don't appear in database
- Check Prisma Client is initialized
- Verify database connection string in
.env - Run migrations:
yarn rw prisma migrate dev - Check for duplicate
messageBirdIdconstraint violations
Problem: Outbound SMS fails with 401
- Verify API key in environment variables
- Use a live key (not test key) for actual sending
- Check API key permissions in MessageBird dashboard
Problem: ngrok tunnel disconnects
- ngrok v3+ requires authentication
- Register at ngrok.com and add authtoken:
ngrok config add-authtoken YOUR_TOKEN - Restart ngrok and update webhook URL in MessageBird dashboard
Problem: SMS character limit exceeded
- Messages over 160 characters (GSM-7) or 70 characters (Unicode) split into multiple SMS
- Implement a character counter in your UI
- Warn users when approaching limits
8. Deployment Considerations
Deploy your RedwoodJS SMS application to production.
Environment Variables
Configure production environment variables in your hosting platform:
MESSAGEBIRD_ACCESS_KEY=live_your_production_key
MESSAGEBIRD_SIGNING_KEY=your_production_signing_key
MESSAGEBIRD_ORIGINATOR_NUMBER=+14155552671
DATABASE_URL=postgresql://user:password@host:5432/databasePlatform-specific configuration:
- Vercel: Project Settings → Environment Variables. Add variables for Production, Preview, and Development
- Netlify: Site Settings → Build & Deploy → Environment. Set variables and deploy contexts
- AWS/Render: Use platform-specific environment variable management UI
- Railway: Add variables in project dashboard Variables tab
Webhook URL Configuration
Update your MessageBird webhook URL to point to your production domain:
- Deploy your application to get the production URL (e.g.,
https://your-app.vercel.app) - Navigate to MessageBird Dashboard → Flow Builder
- Select your flow associated with your virtual number
- Update "Call HTTP endpoint with SMS" step
- Set webhook URL to:
https://your-app.vercel.app/.redwood/functions/webhook - Click Publish to save changes
- Test the webhook with a real SMS to verify connectivity
Zero-downtime updates: When deploying updates:
- Deploy new version to staging environment first
- Test webhook with staging URL
- Deploy to production
- Update webhook URL in MessageBird (< 1 minute downtime)
- Or use webhook URL routing to switch between versions
HTTPS Requirements
MessageBird requires HTTPS for all webhook endpoints.
Automatic HTTPS support:
- Vercel, Netlify, and Railway provide automatic HTTPS with free SSL certificates
- Custom domains require SSL certificate configuration (usually automatic)
- Self-hosted: Use Let's Encrypt for free SSL certificates via Certbot
Verify HTTPS is working: curl -I https://your-domain.com/.redwood/functions/webhook
Database Migrations
Apply database migrations before deploying new code:
# Production migration command (does not prompt)
yarn rw prisma migrate deployCI/CD pipeline configuration (GitHub Actions example):
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: yarn install
- name: Run database migrations
run: yarn rw prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- name: Build application
run: yarn rw build
- name: Deploy to Vercel
run: vercel --prod --token=${{ secrets.VERCEL_TOKEN }}Monitoring and Logging
Implement monitoring to track SMS delivery and webhook processing.
Log critical events:
- Outbound SMS failures (API errors, invalid numbers)
- Webhook signature verification failures (potential attacks)
- Database write errors (connection issues, constraint violations)
- MessageBird API errors (rate limiting, insufficient balance)
- Webhook timeout errors (processing >30 seconds)
Key metrics to track:
- SMS delivery success rate (delivered / sent)
- Webhook processing latency (p50, p95, p99)
- Error rate by type (authentication, validation, API)
- API response times
- Database query performance
Recommended monitoring tools:
- Sentry: Error tracking and performance monitoring (sentry.io)
- LogRocket: Session replay and error tracking for full-stack apps
- Datadog: Application performance monitoring with dashboards
- CloudWatch/Cloud Logging: Cloud platform native monitoring
RedwoodJS logging configuration:
// api/src/lib/logger.ts
import { createLogger } from '@redwoodjs/api/logger'
export const logger = createLogger({
options: {
level: process.env.LOG_LEVEL || 'info',
redact: ['MESSAGEBIRD_ACCESS_KEY', 'MESSAGEBIRD_SIGNING_KEY'],
prettyPrint: process.env.NODE_ENV === 'development'
}
})Use in your code:
import { logger } from 'src/lib/logger'
logger.info({ messageId: response.id }, 'SMS sent successfully')
logger.error({ error: err }, 'Webhook signature verification failed')Rate Limiting
Implement rate limiting to prevent API abuse and protect your budget:
# Install rate limiting library
yarn workspace api add limiter// api/src/functions/webhook/webhook.ts
import { RateLimiterMemory } from 'rate-limiter-flexible';
// Allow 100 webhook requests per minute per IP
const rateLimiter = new RateLimiterMemory({
points: 100,
duration: 60,
});
export const handler = async (event, context) => {
// Extract IP address
const ip = event.headers['x-forwarded-for']?.split(',')[0] ||
event.requestContext?.identity?.sourceIp ||
'unknown';
try {
// Consume 1 point per request
await rateLimiter.consume(ip);
} catch (rejRes) {
console.warn(`Rate limit exceeded for IP: ${ip}`);
return {
statusCode: 429,
body: JSON.stringify({ error: 'Too many requests' })
};
}
// ... existing webhook code ...
};Rate limit recommendations:
- Webhooks: 100 requests/minute per IP (adjust based on expected traffic)
- SMS sending: 10 requests/minute per user (prevent abuse)
- MessageBird API limits: 500 POST requests/second (SMS API) (official documentation)
9. Compliance and Legal Considerations
CRITICAL: SMS marketing is heavily regulated. Non-compliance can result in fines of $500+ per message.
TCPA Compliance (United States)
The Telephone Consumer Protection Act (TCPA) requires:
-
Obtain Express Written Consent: Get explicit written permission before sending marketing SMS. Customers must opt-in via web form, keyword, or checkbox (TCPA guide)
-
Provide Clear Disclosure: After opt-in, send a disclosure message:
[Your Business]: You've subscribed to marketing messages. Msg & data rates may apply. Msg frequency varies. Reply STOP to cancel, HELP for help. Terms: yoursite.com/terms -
Honor Opt-Out Requests: Process STOP, UNSUBSCRIBE, CANCEL, END, and QUIT keywords within 10 business days (as of April 2025) (TCPA 2025 updates)
-
Respect Quiet Hours: Don't send messages before 8:00 AM or after 9:00 PM recipient's local time
-
Include Business Name: Every message must identify your business
-
Maintain Do-Not-Contact List: Keep records of opt-outs for at least 4 years
Example opt-out implementation:
// api/src/functions/webhook/webhook.ts
const handleOptOut = async (sender: string, messageBody: string) => {
const optOutKeywords = ['STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT'];
if (optOutKeywords.includes(messageBody.trim().toUpperCase())) {
// Add to do-not-contact list
await db.optOut.create({
data: { phoneNumber: sender, optedOutAt: new Date() }
});
// Send confirmation
await sendSMS({
to: sender,
body: 'You have been unsubscribed. You will not receive further messages.',
originator: process.env.MESSAGEBIRD_ORIGINATOR_NUMBER
});
return true;
}
return false;
};International Regulations
- GDPR (EU): Obtain consent, provide privacy policy, honor deletion requests
- Canada (CASL): Requires express consent for commercial messages
- Australia (Spam Act): Consent required, must include business identity and unsubscribe method
Cost Considerations
MessageBird uses pay-as-you-go pricing:
- SMS pricing varies by country (typically $0.02-$0.10 per message)
- Concatenated messages (>160 characters) cost multiple credits
- Virtual numbers have monthly fees ($1-$10/month depending on country)
- Check current pricing: messagebird.com/pricing
10. Conclusion
You have successfully integrated two-way SMS messaging into your RedwoodJS application using MessageBird. You can send messages via GraphQL and receive incoming messages through a secure webhook function, with all communication logged to your database.
What you built:
- Outbound SMS via GraphQL with validation and error handling
- Inbound SMS webhook with signature verification and replay attack prevention
- Message logging with database persistence and idempotency
- Security best practices including environment variable management and rate limiting
Next steps:
- Implement auto-responses based on message content keywords
- Add message templates for common notifications
- Build conversation threading and history UI
- Integrate with user authentication system
- Add delivery status tracking via MessageBird status reports
- Implement SMS-based two-factor authentication (2FA)
- Set up monitoring and alerting for delivery failures
Related topics:
- MMS (multimedia messages) with MessageBird
- Voice calls integration
- WhatsApp Business API
- Multi-language SMS support
Prioritize security (especially webhook verification), handle errors gracefully, comply with TCPA and international regulations, and monitor SMS costs and delivery rates.
Resources: