Build SMS Marketing Campaigns with Sinch, Node.js, Vite & React/Vue (2025)
Complete guide to building SMS marketing campaigns using Sinch API, Node.js, Express, and Vite with React or Vue. Learn bulk sending, contact management, delivery tracking, and webhooks for production campaigns.
This comprehensive guide teaches you how to build a production-ready SMS marketing campaign system using Sinch SMS API, Node.js/Express backend, and Vite-powered React or Vue.js frontend. You'll learn to implement bulk SMS sending, contact list management, campaign scheduling, delivery tracking, and real-time status updates.
By the end of this tutorial, you'll have a complete marketing campaign platform featuring: bulk message broadcasting to up to 1,000 recipients per request, webhook-based delivery reporting, contact database integration, frontend dashboards built with Vite and React/Vue, and production-ready error handling and security.
Project Overview and Campaign Architecture
Goal: Build a full-stack SMS marketing campaign system with a Node.js/Express backend for Sinch API integration and a modern Vite/React/Vue frontend for campaign management.
Problem Solved: This application enables businesses to send targeted SMS marketing campaigns to large contact lists, track delivery status in real-time, manage unsubscribe requests, and maintain compliance with SMS marketing regulations. Perfect for promotional campaigns, event notifications, and customer engagement.
Technologies Used:
Node.js (v18+): JavaScript runtime for server-side execution. Required for Express v5.1.0 compatibility.
Express (v5.1.0): Web application framework for building RESTful APIs. Current version as of January 2025 with official LTS support.
Sinch SMS API: Global SMS delivery platform supporting up to 1,000 recipients per batch request (2024 limit increase from previous 100-recipient cap).
Vite (v5.x): Next-generation frontend build tool providing instant HMR and optimized production builds.
React (v18+) or Vue (v3+): Modern JavaScript frameworks for building interactive campaign management interfaces.
node-fetch (v3+) or native fetch: HTTP client for Sinch API requests. Node.js 18+ includes native fetch support.
dotenv (v17.2.3): Environment variable management for secure credential storage.
Expose your local backend to receive Sinch webhooks:
Bash
# Install ngrok globally
npm install -g ngrok
# Start your backend servercd backend
npm run dev
# In another terminal, expose port 3001
ngrok http 3001
Copy the HTTPS URL from ngrok (e.g., https://abc123.ngrok.io) and update your .env:
Bash
BASE_URL=https://abc123.ngrok.io
Restart your backend to use the new webhook URL.
5. Production Deployment Considerations
Security Best Practices
API Authentication: Add JWT tokens or API keys to protect campaign endpoints
Rate Limiting: Implement express-rate-limit to prevent abuse
Input Validation: Use express-validator for comprehensive input checking
HTTPS Only: Deploy behind reverse proxy with TLS/SSL certificates
Environment Variables: Use platform-specific secret managers (AWS Secrets Manager, etc.)
Database Integration
For production campaigns, integrate a database for:
SQL
-- PostgreSQL schema exampleCREATE TABLE campaigns (
id UUID PRIMARY KEYDEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
batch_id VARCHAR(100) UNIQUE,
client_reference VARCHAR(100) UNIQUE,
recipient_count INTEGER,
status VARCHAR(50) DEFAULT'pending',
created_at TIMESTAMPTZ DEFAULTCURRENT_TIMESTAMP
);
CREATE TABLE campaign_recipients (
id UUID PRIMARY KEYDEFAULT gen_random_uuid(),
campaign_id UUID REFERENCES campaigns(id),
phone_number VARCHAR(20) NOT NULL,
delivery_status VARCHAR(50) DEFAULT'pending',
delivery_code INTEGER,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULTCURRENT_TIMESTAMP
);
CREATE INDEX idx_campaign_batch ON campaigns(batch_id);
CREATE INDEX idx_recipient_phone ON campaign_recipients(phone_number);
Deployment Platforms
Backend Options:
Heroku: Easy deployment with automatic SSL
AWS Elastic Beanstalk: Scalable Node.js hosting
Google Cloud Run: Serverless container deployment
DigitalOcean App Platform: Simple deployment with managed databases
Frontend Options:
Vercel: Optimized for Vite/React/Vue with automatic deployment
Netlify: Static site hosting with CDN
Cloudflare Pages: Global edge network deployment
6. Advanced Campaign Features
Contact List Management
Add contact import functionality:
JavaScript
// backend/routes/contactRoutes.js
router.post('/contacts/import', async (req, res) => {
const { contacts } = req.body; // Array of { phone, firstName, lastName, tags }// Validate and deduplicateconst validContacts = contacts.filter(c =>/^\+[1-9]\d{1,14}$/.test(c.phone)
);
// Save to database with opt-in status// TODO: Insert into contacts table
res.json({
imported: validContacts.length,
duplicates: contacts.length - validContacts.length
});
});
// backend/routes/webhookRoutes.js
router.post('/inbound-messages', async (req, res) => {
const { from, body } = req.body;
if (/\b(stop|unsubscribe|end)\b/i.test(body)) {
// Add to suppression listawaitaddToSuppressionList(from);
// Send confirmationawaitsendBulkSms([from], 'You have been unsubscribed. Reply START to opt back in.');
}
res.sendStatus(200);
});
Frequently Asked Questions
How do I send bulk SMS campaigns with Sinch and Node.js?
Send bulk SMS campaigns with Sinch by using their batches endpoint, which accepts up to 1,000 recipients per request. Install the necessary packages (express, node-fetch), configure your Sinch Service Plan ID and API Token, create a POST endpoint that calls the Sinch API with an array of E.164-formatted phone numbers, and implement webhook handlers to track delivery status. This guide demonstrates the complete implementation with Express v5 and Node.js v18+.
What's the maximum number of recipients per Sinch SMS campaign?
Sinch supports a maximum of 1,000 recipients per batch request as of 2024 (increased from the previous 100-recipient limit). For campaigns exceeding 1,000 recipients, implement batch processing by splitting your contact list into chunks of 1,000 or fewer, sending each batch sequentially with appropriate rate-limiting delays to comply with Sinch's throughput restrictions.
How do I integrate Vite with a Node.js SMS campaign backend?
Integrate Vite with Node.js by running them as separate services: start your Express backend on port 3001 and your Vite dev server on port 5173 (default). Use axios or fetch in your React/Vue components to make API calls to http://localhost:3001/api. Configure CORS in your Express app with app.use(cors()) to allow cross-origin requests from the Vite dev server during development.
What are Sinch delivery reports and how do webhooks work?
Sinch delivery reports are HTTP POST callbacks sent to your configured webhook URL when message delivery status changes. Include a callback_url in your batch send request, and Sinch will POST delivery updates containing status (Delivered, Failed), error codes, recipient phone numbers, and your client_reference identifier. Always respond with HTTP 200 to acknowledge receipt and prevent Sinch from retrying the webhook.
Should I use React or Vue for SMS campaign management UI?
Both React and Vue work excellently with Vite for SMS campaign interfaces. React offers a larger ecosystem and more third-party libraries, while Vue provides simpler syntax and faster learning curve. This guide includes complete examples for both frameworks. Choose React if you need extensive component libraries; choose Vue if you prefer template-based components and built-in state management.
How do I handle SMS marketing compliance and opt-outs?
Handle SMS marketing compliance by: (1) obtaining explicit opt-in consent before adding contacts, (2) implementing STOP/UNSUBSCRIBE keyword handling via Sinch inbound webhooks, (3) maintaining a suppression list in your database and filtering it before sending campaigns, (4) including sender identification and opt-out instructions in messages, and (5) respecting regional regulations like TCPA (US), GDPR (EU), and carrier guidelines for commercial messaging.
What database should I use for contact and campaign management?
Use PostgreSQL for relational contact and campaign data with complex querying needs, or MongoDB for flexible schema designs. Create tables/collections for: campaigns (name, message, status, timestamps), campaign_recipients (phone numbers, delivery status), contacts (phone, name, opt-in status, tags), and suppression_list (opted-out numbers). Index phone numbers and batch IDs for fast webhook lookups.
How do I test Sinch webhooks locally during development?
Test Sinch webhooks locally using ngrok to expose your localhost to the internet. Install ngrok (npm install -g ngrok), run ngrok http 3001 to get a public HTTPS URL, update your .env file with BASE_URL=https://your-ngrok-url.ngrok.io, and restart your server. Sinch will now send delivery reports to your local development environment. Remember that ngrok URLs change on restart, so update your .env each time.
What Node.js version is required for this Sinch marketing system?
This system requires Node.js v18.0.0 or higher for Express v5 compatibility and native fetch support. Node.js v18+ includes experimental fetch (stable in v21+), eliminating the need for node-fetch. Use Node.js v22.x (current LTS, active until October 2025) or v24.x (released May 2025) for production deployments with full LTS support and security updates.
How much does it cost to send SMS campaigns with Sinch?
Sinch SMS pricing varies by destination country and volume. US/Canada messages typically cost $0.0045-$0.015 per SMS, with bulk discounts available for high-volume senders. Check Sinch Pricing for current rates. New accounts receive free trial credits. Monitor your usage in the Sinch Customer Dashboard and implement rate limiting in your application to prevent unexpected charges from runaway campaigns.
Frequently asked questions
Use the Vonage Messages API and Node.js SDK. Set up an Express server with an endpoint, initialize the Vonage SDK with credentials, and use the vonage.messages.send() method to send SMS from your Vonage number.
The Vonage Messages API is a versatile communication platform enabling message delivery across various channels like SMS, WhatsApp, and Viber. It allows developers to send and receive messages programmatically.
Dotenv helps manage environment variables by storing sensitive information like Vonage API keys and secrets in a .env file, keeping them separate from your codebase and preventing accidental exposure in version control.
Use the Messages API when you need multi-channel messaging (SMS, WhatsApp, Viber). If you only need basic SMS, the SMS API (vonage.sms.send()) with API key/secret authentication is sufficient.
No, trial accounts require adding recipient numbers to a whitelist in the Vonage dashboard under "Sandbox & Test Numbers" -> "Test Numbers" before sending SMS to those numbers.
In the Vonage Dashboard, create a new application, generate keys (save private.key), enable "Messages" capability, set dummy webhook URLs, generate the application, and link your Vonage virtual number. Critically, ensure the default SMS API setting is "Messages API".
The Vonage Application ID is used for authentication and configuration with the Messages API. Along with the private.key, it grants access to API functionalities when initializing the Vonage Node.js SDK.
Implement robust error handling with try...catch blocks, logging detailed error messages, especially err.response.data from the Vonage SDK. For production, use structured logging libraries (Pino, Winston) and consider retry mechanisms with exponential backoff.
Use environment variables, strong input validation libraries (joi, express-validator), rate limiting middleware (express-rate-limit), and require authentication/authorization (API keys, JWTs, OAuth) for your API endpoint. Always use HTTPS in production.
The E.164 format is an international standard for phone numbers. It includes a plus sign (+) followed by the country code and national number, for example, +14155550100. Use this format for both VONAGE_NUMBER and recipient numbers.
This error usually occurs with trial accounts when sending to numbers not registered in the Vonage Dashboard's Sandbox Test Numbers. Add the recipient number to the whitelist to resolve this.
Double-check Vonage credentials (Application ID, private key path, number), ensure "Messages API" is set as the default in the dashboard, check private key file permissions, use E.164 format, and carefully examine error logs for details from the Vonage SDK.
Choose a platform (Heroku, AWS, GCP, etc.), configure environment variables securely, handle the private.key securely (secrets manager or build process), use a process manager (e.g., pm2), and enforce HTTPS for all traffic.
This comprehensive guide teaches you how to build a production-ready SMS marketing campaign system using Sinch SMS API, Node.js/Express backend, and Vite-powered React or Vue.js frontend. You'll learn to implement bulk SMS sending, contact list management, campaign scheduling, delivery tracking, and real-time status updates.
By the end of this tutorial, you'll have a complete marketing campaign platform featuring: bulk message broadcasting to up to 1,000 recipients per request, webhook-based delivery reporting, contact database integration, frontend dashboards built with Vite and React/Vue, and production-ready error handling and security.
Project Overview and Campaign Architecture
Goal: Build a full-stack SMS marketing campaign system with a Node.js/Express backend for Sinch API integration and a modern Vite/React/Vue frontend for campaign management.
Problem Solved: This application enables businesses to send targeted SMS marketing campaigns to large contact lists, track delivery status in real-time, manage unsubscribe requests, and maintain compliance with SMS marketing regulations. Perfect for promotional campaigns, event notifications, and customer engagement.
Technologies Used:
Marketing Campaign Architecture:
Prerequisites:
1. Backend Setup: Express API with Sinch Integration
Initialize Node.js Project
Create the project structure for both backend and frontend:
mkdir sinch-marketing-platform cd sinch-marketing-platform # Backend setup mkdir backend cd backend npm init -y npm install express dotenv node-fetch corsFor Node.js 18+ using native fetch:
Configure Environment Variables
Create
.envfile for Sinch credentials:# backend/.env # Sinch API Configuration SINCH_SERVICE_PLAN_ID=your_service_plan_id SINCH_API_TOKEN=your_api_token SINCH_BASE_URL=https://us.sms.api.sinch.com SINCH_NUMBER=+1234567890 # Server Configuration PORT=3001 NODE_ENV=development # Webhook Configuration BASE_URL=https://your-ngrok-url.ngrok.ioBuild Sinch Service Layer
Create
backend/services/sinchService.js:// backend/services/sinchService.js import fetch from 'node-fetch'; // or use native fetch in Node 18+ const { SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, SINCH_BASE_URL, SINCH_NUMBER } = process.env; /** * Send bulk SMS campaign via Sinch API * @param {string[]} recipients - Array of phone numbers in E.164 format (max 1000) * @param {string} message - Campaign message content * @param {string} callbackUrl - Webhook URL for delivery reports * @param {string} clientRef - Unique campaign identifier * @returns {Promise<object>} Sinch API response with batch_id */ export async function sendBulkSms(recipients, message, callbackUrl, clientRef) { if (!recipients || recipients.length === 0) { throw new Error('Recipients array cannot be empty'); } if (recipients.length > 1000) { throw new Error('Sinch API maximum is 1000 recipients per batch request'); } const endpoint = `${SINCH_BASE_URL}/xms/v1/${SINCH_SERVICE_PLAN_ID}/batches`; const payload = { from: SINCH_NUMBER, to: recipients, body: message, delivery_report: 'full', callback_url: callbackUrl, client_reference: clientRef }; try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${SINCH_API_TOKEN}` }, body: JSON.stringify(payload) }); const data = await response.json(); if (!response.ok) { throw new Error(`Sinch API Error: ${response.status} - ${JSON.stringify(data)}`); } return data; // Contains batch_id, status, etc. } catch (error) { console.error('Sinch API call failed:', error); throw error; } } /** * Get campaign status from Sinch * @param {string} batchId - Batch ID from send response * @returns {Promise<object>} Campaign delivery statistics */ export async function getCampaignStatus(batchId) { const endpoint = `${SINCH_BASE_URL}/xms/v1/${SINCH_SERVICE_PLAN_ID}/batches/${batchId}`; const response = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${SINCH_API_TOKEN}` } }); return response.json(); }Create Campaign API Endpoints
Create
backend/routes/campaignRoutes.js:// backend/routes/campaignRoutes.js import express from 'express'; import { randomUUID } from 'crypto'; import { sendBulkSms, getCampaignStatus } from '../services/sinchService.js'; const router = express.Router(); // POST /api/campaigns/send - Send bulk SMS campaign router.post('/send', async (req, res) => { const { recipients, message, campaignName } = req.body; // Input validation if (!recipients || !Array.isArray(recipients) || recipients.length === 0) { return res.status(400).json({ error: 'Recipients array is required' }); } if (!message || message.trim().length === 0) { return res.status(400).json({ error: 'Message content is required' }); } // Validate E.164 format const invalidNumbers = recipients.filter(num => !/^\+[1-9]\d{1,14}$/.test(num)); if (invalidNumbers.length > 0) { return res.status(400).json({ error: 'Invalid phone number format', invalidNumbers }); } try { const clientRef = randomUUID(); const callbackUrl = `${process.env.BASE_URL}/api/webhooks/delivery-reports`; const result = await sendBulkSms(recipients, message, callbackUrl, clientRef); res.status(202).json({ success: true, message: 'Campaign accepted for delivery', batchId: result.id, clientRef, recipientCount: recipients.length, details: result }); } catch (error) { console.error('Campaign send error:', error); res.status(500).json({ error: error.message }); } }); // GET /api/campaigns/status/:batchId - Check campaign delivery status router.get('/status/:batchId', async (req, res) => { try { const status = await getCampaignStatus(req.params.batchId); res.json(status); } catch (error) { res.status(500).json({ error: error.message }); } }); export default router;Implement Webhook Handler for Delivery Reports
Create
backend/routes/webhookRoutes.js:// backend/routes/webhookRoutes.js import express from 'express'; const router = express.Router(); // POST /api/webhooks/delivery-reports - Sinch delivery callback router.post('/delivery-reports', async (req, res) => { const report = req.body; console.log('📨 Delivery Report Received:', { recipient: report.recipient, status: report.status, code: report.code, clientRef: report.client_reference, batchId: report.batch_id, timestamp: report.operator_status_at }); // Process delivery report // TODO: Update database with delivery status // TODO: Emit real-time updates to frontend via WebSocket/SSE if (report.status === 'Delivered') { console.log(`✅ Message delivered to ${report.recipient}`); } else if (report.status === 'Failed') { console.error(`❌ Delivery failed to ${report.recipient}: Code ${report.code}`); } // Always acknowledge receipt to Sinch res.status(200).json({ message: 'Webhook received' }); }); export default router;Configure Express Server
Create
backend/server.js:// backend/server.js import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import campaignRoutes from './routes/campaignRoutes.js'; import webhookRoutes from './routes/webhookRoutes.js'; const app = express(); const PORT = process.env.PORT || 3001; // Middleware app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Routes app.get('/health', (req, res) => { res.json({ status: 'healthy', service: 'Sinch Marketing API' }); }); app.use('/api/campaigns', campaignRoutes); app.use('/api/webhooks', webhookRoutes); // Error handling app.use((err, req, res, next) => { console.error('Server error:', err); res.status(500).json({ error: 'Internal server error' }); }); app.listen(PORT, () => { console.log(`🚀 Sinch Marketing API running on port ${PORT}`); console.log(`📍 Webhook endpoint: ${process.env.BASE_URL}/api/webhooks/delivery-reports`); });Update
backend/package.jsonto enable ES modules:{ "type": "module", "scripts": { "start": "node server.js", "dev": "node --watch server.js" } }2. Frontend Setup: Vite + React Campaign Dashboard
Initialize Vite Project with React
cd .. npm create vite@latest frontend -- --template react cd frontend npm install npm install axiosFor Vue instead of React:
Build Campaign Management Component
Create
frontend/src/components/CampaignManager.jsx:// frontend/src/components/CampaignManager.jsx import { useState } from 'react'; import axios from 'axios'; const API_BASE = 'http://localhost:3001/api'; export default function CampaignManager() { const [recipients, setRecipients] = useState(''); const [message, setMessage] = useState(''); const [campaignName, setCampaignName] = useState(''); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const handleSendCampaign = async (e) => { e.preventDefault(); setLoading(true); setResult(null); try { // Parse recipients (one per line) const recipientList = recipients .split('\n') .map(num => num.trim()) .filter(num => num.length > 0); const response = await axios.post(`${API_BASE}/campaigns/send`, { recipients: recipientList, message, campaignName }); setResult({ success: true, data: response.data }); // Clear form on success setRecipients(''); setMessage(''); setCampaignName(''); } catch (error) { setResult({ success: false, error: error.response?.data?.error || error.message }); } finally { setLoading(false); } }; return ( <div className="campaign-manager"> <h1>📱 SMS Marketing Campaign</h1> <form onSubmit={handleSendCampaign} className="campaign-form"> <div className="form-group"> <label>Campaign Name</label> <input type="text" value={campaignName} onChange={(e) => setCampaignName(e.target.value)} placeholder="e.g., Spring Sale 2025" required /> </div> <div className="form-group"> <label>Recipients (E.164 format, one per line)</label> <textarea value={recipients} onChange={(e) => setRecipients(e.target.value)} placeholder="+14155551234 +14155555678" rows={8} required /> <small>Maximum 1,000 recipients per campaign</small> </div> <div className="form-group"> <label>Message Content</label> <textarea value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Your marketing message here..." rows={4} maxLength={1600} required /> <small>{message.length}/1600 characters</small> </div> <button type="submit" disabled={loading} className="btn-primary" > {loading ? 'Sending...' : 'Send Campaign'} </button> </form> {result && ( <div className={`result ${result.success ? 'success' : 'error'}`}> {result.success ? ( <div> <h3>✅ Campaign Sent Successfully!</h3> <p><strong>Batch ID:</strong> {result.data.batchId}</p> <p><strong>Recipients:</strong> {result.data.recipientCount}</p> <p><strong>Reference:</strong> {result.data.clientRef}</p> </div> ) : ( <div> <h3>❌ Campaign Failed</h3> <p>{result.error}</p> </div> )} </div> )} </div> ); }Add Campaign Status Tracker
Create
frontend/src/components/CampaignStatus.jsx:// frontend/src/components/CampaignStatus.jsx import { useState } from 'react'; import axios from 'axios'; const API_BASE = 'http://localhost:3001/api'; export default function CampaignStatus() { const [batchId, setBatchId] = useState(''); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const checkStatus = async () => { if (!batchId) return; setLoading(true); try { const response = await axios.get(`${API_BASE}/campaigns/status/${batchId}`); setStatus(response.data); } catch (error) { console.error('Status check failed:', error); setStatus({ error: 'Failed to fetch status' }); } finally { setLoading(false); } }; return ( <div className="campaign-status"> <h2>📊 Campaign Status Tracker</h2> <div className="status-lookup"> <input type="text" value={batchId} onChange={(e) => setBatchId(e.target.value)} placeholder="Enter Batch ID" /> <button onClick={checkStatus} disabled={loading}> {loading ? 'Checking...' : 'Check Status'} </button> </div> {status && !status.error && ( <div className="status-details"> <h3>Campaign Details</h3> <p><strong>Status:</strong> {status.canceled ? 'Canceled' : 'Active'}</p> <p><strong>Recipients:</strong> {status.to?.length || 0}</p> <p><strong>Message:</strong> {status.body}</p> <p><strong>Created:</strong> {new Date(status.created_at).toLocaleString()}</p> </div> )} </div> ); }Main App Component
Update
frontend/src/App.jsx:// frontend/src/App.jsx import CampaignManager from './components/CampaignManager'; import CampaignStatus from './components/CampaignStatus'; import './App.css'; function App() { return ( <div className="app"> <header> <h1>Sinch SMS Marketing Platform</h1> <p>Powered by Vite + React</p> </header> <main> <CampaignManager /> <CampaignStatus /> </main> </div> ); } export default App;Add Styling
Create
frontend/src/App.css:/* frontend/src/App.css */ .app { max-width: 1200px; margin: 0 auto; padding: 2rem; } header { text-align: center; margin-bottom: 3rem; padding-bottom: 2rem; border-bottom: 2px solid #e0e0e0; } .campaign-manager, .campaign-status { background: #fff; padding: 2rem; margin-bottom: 2rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .campaign-form { display: flex; flex-direction: column; gap: 1.5rem; } .form-group { display: flex; flex-direction: column; gap: 0.5rem; } .form-group label { font-weight: 600; color: #333; } .form-group input, .form-group textarea { padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-family: inherit; font-size: 1rem; } .form-group small { color: #666; font-size: 0.875rem; } .btn-primary { padding: 1rem 2rem; background: #0066cc; color: white; border: none; border-radius: 4px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: background 0.2s; } .btn-primary:hover { background: #0052a3; } .btn-primary:disabled { background: #ccc; cursor: not-allowed; } .result { margin-top: 2rem; padding: 1.5rem; border-radius: 4px; } .result.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; } .result.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }3. Vue.js Alternative Implementation
For Vue developers, here's the equivalent Campaign Manager component:
4. Testing with ngrok for Webhook Development
Expose your local backend to receive Sinch webhooks:
# Install ngrok globally npm install -g ngrok # Start your backend server cd backend npm run dev # In another terminal, expose port 3001 ngrok http 3001Copy the HTTPS URL from ngrok (e.g.,
https://abc123.ngrok.io) and update your.env:Restart your backend to use the new webhook URL.
5. Production Deployment Considerations
Security Best Practices
express-rate-limitto prevent abuseexpress-validatorfor comprehensive input checkingDatabase Integration
For production campaigns, integrate a database for:
-- PostgreSQL schema example CREATE TABLE campaigns ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, message TEXT NOT NULL, batch_id VARCHAR(100) UNIQUE, client_reference VARCHAR(100) UNIQUE, recipient_count INTEGER, status VARCHAR(50) DEFAULT 'pending', created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE campaign_recipients ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), campaign_id UUID REFERENCES campaigns(id), phone_number VARCHAR(20) NOT NULL, delivery_status VARCHAR(50) DEFAULT 'pending', delivery_code INTEGER, delivered_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_campaign_batch ON campaigns(batch_id); CREATE INDEX idx_recipient_phone ON campaign_recipients(phone_number);Deployment Platforms
Backend Options:
Frontend Options:
6. Advanced Campaign Features
Contact List Management
Add contact import functionality:
// backend/routes/contactRoutes.js router.post('/contacts/import', async (req, res) => { const { contacts } = req.body; // Array of { phone, firstName, lastName, tags } // Validate and deduplicate const validContacts = contacts.filter(c => /^\+[1-9]\d{1,14}$/.test(c.phone) ); // Save to database with opt-in status // TODO: Insert into contacts table res.json({ imported: validContacts.length, duplicates: contacts.length - validContacts.length }); });Campaign Scheduling
Implement scheduled campaign sending:
// backend/services/schedulerService.js import cron from 'node-cron'; export function scheduleCampaign(campaignId, sendTime) { const cronTime = convertToCronExpression(sendTime); cron.schedule(cronTime, async () => { const campaign = await getCampaignById(campaignId); await sendBulkSms(campaign.recipients, campaign.message, ...); console.log(`Scheduled campaign ${campaignId} sent`); }); }Opt-Out/Unsubscribe Management
Handle STOP keywords via Sinch inbound webhooks:
// backend/routes/webhookRoutes.js router.post('/inbound-messages', async (req, res) => { const { from, body } = req.body; if (/\b(stop|unsubscribe|end)\b/i.test(body)) { // Add to suppression list await addToSuppressionList(from); // Send confirmation await sendBulkSms([from], 'You have been unsubscribed. Reply START to opt back in.'); } res.sendStatus(200); });Frequently Asked Questions
How do I send bulk SMS campaigns with Sinch and Node.js?
Send bulk SMS campaigns with Sinch by using their batches endpoint, which accepts up to 1,000 recipients per request. Install the necessary packages (
express,node-fetch), configure your Sinch Service Plan ID and API Token, create a POST endpoint that calls the Sinch API with an array of E.164-formatted phone numbers, and implement webhook handlers to track delivery status. This guide demonstrates the complete implementation with Express v5 and Node.js v18+.What's the maximum number of recipients per Sinch SMS campaign?
Sinch supports a maximum of 1,000 recipients per batch request as of 2024 (increased from the previous 100-recipient limit). For campaigns exceeding 1,000 recipients, implement batch processing by splitting your contact list into chunks of 1,000 or fewer, sending each batch sequentially with appropriate rate-limiting delays to comply with Sinch's throughput restrictions.
How do I integrate Vite with a Node.js SMS campaign backend?
Integrate Vite with Node.js by running them as separate services: start your Express backend on port 3001 and your Vite dev server on port 5173 (default). Use
axiosorfetchin your React/Vue components to make API calls tohttp://localhost:3001/api. Configure CORS in your Express app withapp.use(cors())to allow cross-origin requests from the Vite dev server during development.What are Sinch delivery reports and how do webhooks work?
Sinch delivery reports are HTTP POST callbacks sent to your configured webhook URL when message delivery status changes. Include a
callback_urlin your batch send request, and Sinch will POST delivery updates containing status (Delivered,Failed), error codes, recipient phone numbers, and yourclient_referenceidentifier. Always respond with HTTP 200 to acknowledge receipt and prevent Sinch from retrying the webhook.Should I use React or Vue for SMS campaign management UI?
Both React and Vue work excellently with Vite for SMS campaign interfaces. React offers a larger ecosystem and more third-party libraries, while Vue provides simpler syntax and faster learning curve. This guide includes complete examples for both frameworks. Choose React if you need extensive component libraries; choose Vue if you prefer template-based components and built-in state management.
How do I handle SMS marketing compliance and opt-outs?
Handle SMS marketing compliance by: (1) obtaining explicit opt-in consent before adding contacts, (2) implementing STOP/UNSUBSCRIBE keyword handling via Sinch inbound webhooks, (3) maintaining a suppression list in your database and filtering it before sending campaigns, (4) including sender identification and opt-out instructions in messages, and (5) respecting regional regulations like TCPA (US), GDPR (EU), and carrier guidelines for commercial messaging.
What database should I use for contact and campaign management?
Use PostgreSQL for relational contact and campaign data with complex querying needs, or MongoDB for flexible schema designs. Create tables/collections for: campaigns (name, message, status, timestamps), campaign_recipients (phone numbers, delivery status), contacts (phone, name, opt-in status, tags), and suppression_list (opted-out numbers). Index phone numbers and batch IDs for fast webhook lookups.
How do I test Sinch webhooks locally during development?
Test Sinch webhooks locally using ngrok to expose your localhost to the internet. Install ngrok (
npm install -g ngrok), runngrok http 3001to get a public HTTPS URL, update your.envfile withBASE_URL=https://your-ngrok-url.ngrok.io, and restart your server. Sinch will now send delivery reports to your local development environment. Remember that ngrok URLs change on restart, so update your.enveach time.What Node.js version is required for this Sinch marketing system?
This system requires Node.js v18.0.0 or higher for Express v5 compatibility and native fetch support. Node.js v18+ includes experimental fetch (stable in v21+), eliminating the need for
node-fetch. Use Node.js v22.x (current LTS, active until October 2025) or v24.x (released May 2025) for production deployments with full LTS support and security updates.How much does it cost to send SMS campaigns with Sinch?
Sinch SMS pricing varies by destination country and volume. US/Canada messages typically cost $0.0045-$0.015 per SMS, with bulk discounts available for high-volume senders. Check Sinch Pricing for current rates. New accounts receive free trial credits. Monitor your usage in the Sinch Customer Dashboard and implement rate limiting in your application to prevent unexpected charges from runaway campaigns.