This guide provides a step-by-step walkthrough for building a robust Node.js application using the Express framework to send bulk SMS broadcast messages via the MessageBird API. You'll learn how to set up the project, handle large recipient lists efficiently, manage API keys securely, implement error handling, and prepare the application for production deployment.
This solution addresses the need to communicate announcements, alerts, or marketing messages to a large audience simultaneously via SMS, leveraging MessageBird's reliable infrastructure to handle the delivery load. We use Node.js and Express for their asynchronous nature and extensive ecosystem, making them well-suited for I/O-bound tasks like API interactions. MessageBird is chosen for its clear API, developer-friendly SDK, and global reach for SMS delivery.
System Architecture:
+-------------+ +-----------------------+ +-------------------+ +-------------+ +----------------+
| Client |------>| Node.js/Express API |------>| MessageBird API |------>| SMS Network |------>| User Phones |
| (e.g., curl)| | (Your Application) | | | | | | (Recipients) |
+-------------+ +-----------------------+ +-------------------+ +-------------+ +----------------+
| ^
| | (Fetch Recipients)
v |
+----------+
| Database |
| (Optional)|
+----------+
Prerequisites:
- Node.js and npm (or yarn) installed.
- A MessageBird account (Sign up here).
- Your MessageBird Live API Key.
- Basic understanding of Node.js, Express, and REST APIs.
- A text editor or IDE (like VS Code).
- Terminal or command prompt access.
1. Project Setup
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir node-messagebird-broadcast cd node-messagebird-broadcast
-
Initialize Project: Initialize a new Node.js project using npm. The
-y
flag accepts default settings.npm init -y
-
Install Dependencies: Install Express (web framework), the MessageBird Node.js SDK, and
dotenv
for managing environment variables.npm install express messagebird dotenv
-
Install Development Dependencies (Optional but Recommended): Install
nodemon
for automatic server restarts during development.npm install --save-dev nodemon
-
Configure
package.json
Scripts: Openpackage.json
and add scripts for starting the server normally and withnodemon
.// package.json { // ... other configurations ... ""scripts"": { ""start"": ""node server.js"", ""dev"": ""nodemon server.js"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" }, // ... other configurations ... }
-
Create Basic Server File: Create a file named
server.js
in the root directory.// server.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware to parse JSON bodies app.use(express.json()); // Simple root route app.get('/', (req, res) => { res.send('MessageBird Broadcast API is running!'); }); // Placeholder for broadcast routes (we'll add this later) // const broadcastRoutes = require('./routes/broadcastRoutes'); // app.use('/api/broadcast', broadcastRoutes); app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
-
Create Environment File (
.env
): Create a file named.env
in the root directory. Never commit this file to version control. Add your MessageBird API Key here. We'll also add the originator number/name.# .env MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY MESSAGEBIRD_ORIGINATOR=YourSenderNameOrNumber PORT=3000 # Add a simple API key for securing your endpoint (replace with a real secret) INTERNAL_API_KEY=your-secret-api-key
MESSAGEBIRD_API_KEY
: Your Live API key from the MessageBird Dashboard.MESSAGEBIRD_ORIGINATOR
: The phone number (E.164 format, e.g.,+12025550181
) or alphanumeric sender ID (max 11 chars, check country support) that messages will come from.INTERNAL_API_KEY
: A simple key to protect your API endpoint. Use a strong, randomly generated key in production.
-
Create
.gitignore
: Create a.gitignore
file to prevent sensitive files and unnecessary directories from being committed.# .gitignore node_modules/ .env npm-debug.log* yarn-debug.log* yarn-error.log*
-
Project Structure (Recommended): Organize your code for better maintainability.
node-messagebird-broadcast/ ├── node_modules/ ├── config/ │ └── messagebird.js # MessageBird SDK initialization ├── controllers/ │ └── broadcastController.js # Handles request logic ├── routes/ │ └── broadcastRoutes.js # Defines API endpoints ├── services/ │ └── messagebirdService.js # Interacts with MessageBird API ├── middleware/ │ └── authMiddleware.js # API Key authentication ├── .env # Environment variables (DO NOT COMMIT) ├── .gitignore # Git ignore rules ├── package.json ├── package-lock.json └── server.js # Main application file
Create these directories now:
mkdir config controllers routes services middleware
2. Implementing Core Functionality
We'll encapsulate the MessageBird interaction logic within a dedicated service.
-
Initialize MessageBird SDK: Create
config/messagebird.js
to initialize the SDK client using the environment variable.// config/messagebird.js const messagebird = require('messagebird'); const apiKey = process.env.MESSAGEBIRD_API_KEY; if (!apiKey) { console.error('FATAL ERROR: MESSAGEBIRD_API_KEY environment variable is not set.'); process.exit(1); // Exit if the key is missing } const mbClient = messagebird.initClient(apiKey); module.exports = mbClient;
Why initialize here? Centralizing initialization makes it easy to manage the client instance and ensures the API key check happens early.
-
Create MessageBird Service: Create
services/messagebirdService.js
to handle sending the broadcast message.// services/messagebirdService.js const mbClient = require('../config/messagebird'); /** * Sends a broadcast SMS message to multiple recipients using MessageBird. * @param {string} originator - The sender ID or phone number. * @param {string[]} recipients - An array of recipient phone numbers in E.164 format. * @param {string} body - The message content. * @returns {Promise<object>} - A promise that resolves with the MessageBird API response. * @throws {Error} - Throws an error if the API call fails. */ async function sendBroadcastSms(originator, recipients, body) { console.log(`Attempting to send message from ${originator} to ${recipients.length} recipients.`); const params = { originator: originator, recipients: recipients, body: body, // You can add other optional parameters here, e.g., validity, scheduledDatetime // reference: 'your-internal-broadcast-id-123', // Optional: for tracking }; return new Promise((resolve, reject) => { mbClient.messages.create(params, (err, response) => { if (err) { // Log the detailed error from MessageBird console.error('MessageBird API Error:', JSON.stringify(err, null, 2)); // Create a more structured error for upstream handling const error = new Error(`Failed to send broadcast via MessageBird: ${err.errors ? err.errors[0].description : 'Unknown error'}`); error.statusCode = err.statusCode || 500; error.details = err.errors; return reject(error); } console.log('MessageBird API Success:', JSON.stringify(response, null, 2)); resolve(response); }); }); } module.exports = { sendBroadcastSms, };
Why
async
/Promise
? The MessageBird SDK uses callbacks. Wrapping it in a Promise allows us to use modernasync/await
syntax in our controllers, simplifying asynchronous code flow. The standardmessages.create
endpoint is used, passing the list of recipients directly in therecipients
array. MessageBird handles fanning this out.
3. Building the API Layer
Now, let's create the Express route and controller to expose our broadcast functionality via a REST API endpoint.
-
Create Authentication Middleware: Create
middleware/authMiddleware.js
for basic API key protection.// middleware/authMiddleware.js const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY; if (!INTERNAL_API_KEY) { // Stronger warning about production use console.warn('WARNING: INTERNAL_API_KEY is not set. API endpoint is unprotected.'); console.warn('This behavior is intended for local development only; **never deploy to production without a configured INTERNAL_API_KEY**.'); } function requireApiKey(req, res, next) { // Allow skipping auth if no key is configured (useful for local dev, but log warning) if (!INTERNAL_API_KEY) { return next(); } const apiKey = req.headers['x-api-key']; if (!apiKey || apiKey !== INTERNAL_API_KEY) { console.warn(`Unauthorized access attempt with key: ${apiKey}`); return res.status(401).json({ error: 'Unauthorized: Invalid or missing API key' }); } next(); } module.exports = { requireApiKey };
Security Note: This provides basic protection. For production, consider more robust methods like JWT, OAuth, or mTLS depending on your security requirements.
-
Create Broadcast Controller: Create
controllers/broadcastController.js
to handle incoming requests, validate input, call the service, and send responses.// controllers/broadcastController.js const messagebirdService = require('../services/messagebirdService'); // Basic validation helper (consider using libraries like express-validator for complex validation) function validateRequest(body) { const errors = []; if (!body.message || typeof body.message !== 'string' || body.message.trim() === '') { errors.push('Field ""message"" is required and must be a non-empty string.'); } if (!body.recipients || !Array.isArray(body.recipients) || body.recipients.length === 0) { errors.push('Field ""recipients"" is required and must be a non-empty array of strings.'); } else { // Basic E.164 format check // Note: This regex is a basic check and may not catch all technically invalid E.164 numbers. // Using a dedicated library like `libphonenumber-js` (discussed in Section 7) is recommended for robust validation. const invalidNumbers = body.recipients.filter(num => !/^\+\d{1,15}$/.test(num)); if (invalidNumbers.length > 0) { errors.push(`Invalid phone number format detected for: ${invalidNumbers.join(', ')}. Use E.164 format (e.g., +12025550181).`); } } // Add character limit check (optional, but good practice) if (body.message && body.message.length > 1600) { // Example limit for multi-part SMS errors.push(`Message body exceeds maximum recommended length (e.g., 1600 chars for ~10 parts). Consider shortening.`); } return errors; } async function handleBroadcastSms(req, res) { const validationErrors = validateRequest(req.body); if (validationErrors.length > 0) { return res.status(400).json({ error: 'Bad Request', details: validationErrors }); } const { recipients, message } = req.body; const originator = process.env.MESSAGEBIRD_ORIGINATOR; if (!originator) { console.error('Configuration Error: MESSAGEBIRD_ORIGINATOR is not set in .env'); return res.status(500).json({ error: 'Internal Server Error', details: 'Server configuration missing originator.' }); } // --- Handling Very Large Lists (Chunking) --- // MessageBird's `messages.create` likely has internal limits or best practices. // For very large lists (e.g., > 1000), chunking client-side adds robustness. const MAX_RECIPIENTS_PER_REQUEST = 1000; // Adjust based on testing/MessageBird guidance const recipientChunks = []; for (let i = 0; i < recipients.length; i += MAX_RECIPIENTS_PER_REQUEST) { recipientChunks.push(recipients.slice(i_ i + MAX_RECIPIENTS_PER_REQUEST)); } console.log(`Processing broadcast to ${recipients.length} recipients in ${recipientChunks.length} chunk(s).`); const results = { success: []_ failed: []_ errors: [] }; // Process chunks sequentially to avoid overwhelming the API or hitting rate limits aggressively. // For higher throughput_ consider Promise.allSettled with concurrency control (e.g._ using 'p-limit' library). for (const chunk of recipientChunks) { try { const response = await messagebirdService.sendBroadcastSms(originator_ chunk_ message); console.log(`Successfully submitted chunk of ${chunk.length} messages. MessageBird ID: ${response.id}`); // Store minimal success info_ response can be large results.success.push({ messageId: response.id_ recipientCount: chunk.length_ firstRecipientPreview: chunk[0] // For logging/debugging }); } catch (error) { console.error(`Failed to send chunk: ${error.message}`); results.failed.push({ recipientCount: chunk.length_ firstRecipientPreview: chunk[0]_ error: error.message_ details: error.details || 'No details provided' }); results.errors.push(error); // Keep the full error object if needed } } // --- Response Handling --- if (results.failed.length === recipientChunks.length && recipients.length > 0) { // All chunks failed return res.status(500).json({ status: 'error', message: 'All message chunks failed to send.', details: results.failed }); } else if (results.failed.length > 0) { // Partial success return res.status(207).json({ // 207 Multi-Status status: 'partial_success', message: `Successfully submitted ${results.success.length} chunk(s), but ${results.failed.length} chunk(s) failed.`, success_details: results.success, failure_details: results.failed }); } else { // Full success return res.status(200).json({ status: 'success', message: `Successfully submitted ${results.success.length} chunk(s) for processing by MessageBird.`, details: results.success }); } } module.exports = { handleBroadcastSms, };
Why chunking? While MessageBird's API is robust, sending tens of thousands of recipients in a single API call might hit timeouts or undocumented limits. Breaking the list into smaller chunks (e.g., 1000 recipients each) and sending them sequentially (or with controlled concurrency) makes the process more reliable and manageable for both your server and the MessageBird API.
-
Create Broadcast Routes: Create
routes/broadcastRoutes.js
to define the API endpoint and link it to the controller and middleware.// routes/broadcastRoutes.js const express = require('express'); const broadcastController = require('../controllers/broadcastController'); const { requireApiKey } = require('../middleware/authMiddleware'); const router = express.Router(); // POST /api/broadcast/sms // Requires 'x-api-key' header for authentication // Body: { ""recipients"": [""+1..."", ""+44...""], ""message"": ""Your broadcast text"" } router.post('/sms', requireApiKey, broadcastController.handleBroadcastSms); module.exports = router;
-
Update
server.js
to Use Routes: Uncomment and add the lines inserver.js
to include the routes.// server.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware to parse JSON bodies app.use(express.json()); // --- Add Authentication Middleware and Routes --- const broadcastRoutes = require('./routes/broadcastRoutes'); // Auth is applied at the route level within broadcastRoutes.js app.use('/api/broadcast', broadcastRoutes); // Simple root route app.get('/', (req, res) => { res.send('MessageBird Broadcast API is running!'); }); // Basic Health Check endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); // --- Global Error Handler (Basic Example) --- // Catches errors not handled in specific routes app.use((err, req, res, next) => { console.error(""Unhandled Error:"", err.stack || err); // Fixed quotes const statusCode = err.statusCode || 500; res.status(statusCode).json({ status: 'error', message: err.message || 'An unexpected internal server error occurred.', // Optionally include stack trace in development ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); }); app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
-
Test the Endpoint: Start the server:
npm run dev
Usecurl
or Postman to send a POST request:curl -X POST http://localhost:3000/api/broadcast/sms \ -H ""Content-Type: application/json"" \ -H ""x-api-key: your-secret-api-key"" \ -d '{ ""recipients"": [""+12025550181"", ""+447700900001""], ""message"": ""[Your Test Message Here]"" }'
- Replace
your-secret-api-key
with the value in your.env
. - Replace the phone numbers with valid E.164 formatted numbers you can test with.
- You should receive a JSON response indicating success, partial success, or failure, along with details. Check your test phones for the SMS.
- Replace
4. Integrating with MessageBird (Configuration Details)
We've already integrated the SDK, but let's clarify the key configuration aspects.
- API Key:
- Obtaining: Log in to your MessageBird Dashboard. Navigate to Developer Settings (icon often looks like
< >
or a gear in the bottom-left menu) -> API access. You'll find your Live and Test API keys. Use the Live key for actual sending. - Security: Store the key only in the
.env
file locally and use secure environment variable management in your deployment environment (e.g., platform secrets, secrets manager). Never hardcode it or commit it to Git. Regenerate the key immediately if compromised.
- Obtaining: Log in to your MessageBird Dashboard. Navigate to Developer Settings (icon often looks like
- Originator:
- Obtaining: This is either a virtual mobile number purchased from MessageBird (under Numbers in the dashboard) or an alphanumeric sender ID you define.
- Configuration: Set
MESSAGEBIRD_ORIGINATOR
in your.env
file. - Restrictions: Alphanumeric sender IDs (e.g., ""MyCompany"") have limited support globally (e.g., not typically supported for sending to the US). Using a purchased E.164 formatted number (e.g.,
+12025550181
) is generally more reliable for global delivery. Check MessageBird's country restrictions documentation. - Format: Always use E.164 format for numeric originators (
+
followed by country code and number).
5. Error Handling, Logging, and Retries
Robust error handling is crucial for a production application.
- Error Handling Strategy:
- Validation: Input validation happens first in the controller (
validateRequest
) to catch bad requests early (400 Bad Request). - Service Errors: The
messagebirdService
catches specific API errors from the SDK, logs them with details, and throws a structured error containing a user-friendly message, status code, and details. - Controller Catching: The controller uses
try...catch
around the service call to handle failures during the broadcast attempt (e.g., API key issues, network problems, invalid recipients identified by MessageBird). It differentiates between total and partial failures, returning appropriate status codes (500 Internal Server Error, 207 Multi-Status). - Global Handler: The final
app.use((err, req, res, next))
inserver.js
acts as a catch-all for unexpected errors that might occur elsewhere in the middleware chain.
- Validation: Input validation happens first in the controller (
- Logging:
- We use
console.log
for informational messages (e.g., starting send, success) andconsole.error
for errors. - Production Logging: For production, replace
console
with a dedicated logging library likewinston
orpino
. These enable:- Different log levels (debug, info, warn, error).
- Structured logging (JSON format) for easier parsing by log analysis tools.
- Outputting logs to files, databases, or external services (e.g., Datadog, Logstash).
- What to Log: Log key events (request received, starting broadcast, chunk submission success/failure), errors (with stack traces in dev, without in prod), and relevant context (like recipient count, first recipient in chunk for debugging). Avoid logging sensitive data like full recipient lists or API keys.
- We use
- Retry Mechanisms:
- Network Issues: The MessageBird SDK might handle some transient network retries internally.
- API Errors: Retrying specific MessageBird errors might be necessary.
- Safe to Retry: Temporary server errors (5xx status codes from MessageBird), rate limiting errors (429 Too Many Requests - implement exponential backoff).
- Do Not Retry: Authentication errors (401), validation errors (400/422 - indicates bad input), insufficient balance errors.
- Implementation: For robust retries, use a library like
async-retry
orp-retry
. Wrap themessagebirdService.sendBroadcastSms
call within the retry logic, configuring which errors should trigger a retry and using exponential backoff to avoid overwhelming the API. Keep the current sequential chunk processing simple for this guide, but mention retry libraries as an enhancement.
6. Database Schema and Data Layer (Conceptual)
While this guide focuses on the API, in a real application, recipients would likely come from a database.
-
Schema Example (Users Table):
CREATE TABLE users ( id SERIAL PRIMARY KEY, first_name VARCHAR(100), last_name VARCHAR(100), phone_number VARCHAR(20) UNIQUE NOT NULL, -- Store in E.164 format is_subscribed BOOLEAN DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Index for faster lookups on subscribed users' phone numbers CREATE INDEX idx_users_subscribed_phone ON users (phone_number) WHERE is_subscribed = TRUE;
-
Fetching Recipients: Replace the hardcoded list in the controller with a database query.
// controllers/broadcastController.js (Conceptual Fetching) // Replace this with your actual DB query using your chosen ORM/client async function getSubscribedRecipients() { // Example using a hypothetical ORM 'db' // const users = await db.user.findMany({ // where: { is_subscribed: true }, // select: { phone_number: true } // }); // return users.map(user => user.phone_number); // Placeholder for demonstration: console.warn(""Using placeholder recipient list. Integrate database query.""); return [""+12025550181"", ""+447700900002""]; // Replace with DB call } async function handleBroadcastSms(req, res) { // ... validation ... const { message } = req.body; // Recipients now fetched from DB try { const recipients = await getSubscribedRecipients(); if (recipients.length === 0) { return res.status(404).json({ status: 'not_found', message: 'No subscribed recipients found to send the broadcast to.' }); } const originator = process.env.MESSAGEBIRD_ORIGINATOR; // ... rest of the chunking and sending logic ... } catch (dbError) { console.error(""Database Error fetching recipients:"", dbError); return res.status(500).json({ error: 'Internal Server Error', details: 'Failed to retrieve recipient list.' }); } }
7. Adding Security Features
Beyond basic API key authentication:
- Input Validation & Sanitization:
- We added basic validation. Use libraries like
express-validator
for more complex rules (checking types, lengths, formats, sanitizing inputs to prevent XSS if data is ever displayed). - Specifically validate phone numbers rigorously using a library like
libphonenumber-js
to ensure they are valid and in E.164 format before sending to MessageBird.
- We added basic validation. Use libraries like
- Rate Limiting:
- Protect your API from abuse and brute-force attacks. Use middleware like
express-rate-limit
. - Add it in
server.js
:
// server.js const rateLimit = require('express-rate-limit'); // ... other requires ... const app = express(); // ... // Apply rate limiting to API routes const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); app.use('/api', apiLimiter); // Apply to all routes under /api app.use(express.json()); // ... rest of the setup ...
- Protect your API from abuse and brute-force attacks. Use middleware like
- HTTPS: Always run your production application behind HTTPS to encrypt traffic. Use a reverse proxy like Nginx or Caddy, or leverage platform features (e.g., Heroku, AWS ELB).
- Dependency Security: Regularly audit your dependencies for known vulnerabilities using
npm audit
and update them.
8. Handling Special Cases
- Phone Number Formatting: MessageBird strictly requires E.164 format (
+CountryCodeNumber
). Ensure your input validation or data layer cleans/formats numbers correctly before they reach themessagebirdService
. - Character Limits & Encoding:
- Standard SMS is 160 GSM-7 characters. Longer messages become concatenated (multipart) SMS.
- Using non-GSM characters (like many emojis or specific accented letters) forces UCS-2 encoding, reducing the limit per part to 70 characters.
- MessageBird handles concatenation, but each part is billed as a separate SMS. Be mindful of message length to manage costs. Inform users if their message will be split. Our validation adds a basic length check.
originator
Restrictions: Reiterate checking MessageBird's documentation for sender ID restrictions in target countries.- Delivery Reports (DLRs): This guide focuses on sending. To track the status (delivered, failed, etc.), you need to configure webhooks in MessageBird to send status updates back to another endpoint in your application. This involves creating a separate route/controller to receive and process these POST requests from MessageBird.
- Opt-Out Handling: Implement mechanisms (e.g., keyword handling via webhooks, database flags) to respect user opt-out requests, complying with regulations like TCPA/GDPR.
9. Implementing Performance Optimizations
- Asynchronous Operations: Node.js and our use of
async/await
with Promises are inherently non-blocking, suitable for I/O-bound tasks like API calls. - Chunking: The client-side chunking implemented in the controller prevents overwhelming a single API request and manages load.
- Database Indexing: Ensure your
phone_number
column (and any filtering columns likeis_subscribed
) are properly indexed in the database for fast lookups. - Connection Pooling: If using a database, ensure your ORM or database client uses connection pooling efficiently.
- Caching: If the recipient list doesn't change frequently, consider caching the list (e.g., in Redis or memory cache with TTL) to avoid repeated database queries.
- Load Testing: Use tools like
k6
,Artillery
, orJMeter
to simulate traffic and identify bottlenecks in your API endpoint, database queries, or downstream dependencies (though avoid excessive load testing directly against MessageBird's live API without coordination). Monitor resource usage (CPU, memory) during tests.
10. Monitoring, Observability, and Analytics
- Health Checks: The
/health
endpoint provides a basic check for load balancers or uptime monitors. - Logging: Centralized, structured logging (as mentioned in section 5) is key. Tools like Datadog, Grafana Loki, or ELK Stack can ingest and analyze logs.
- Performance Metrics:
- Use APM (Application Performance Monitoring) tools (e.g., Datadog APM, New Relic, Dynatrace) to automatically instrument your Express app. They track request latency, error rates, throughput, and trace requests through your services (including database calls).
- Monitor Node.js specific metrics (event loop lag, garbage collection, memory usage) using tools like
pm2
's monitoring or dedicated Node.js profilers.
- Error Tracking: Services like Sentry or Bugsnag capture unhandled exceptions and provide detailed context for debugging.
- MessageBird Dashboard: Utilize MessageBird's dashboard analytics (Insights, SMS logs) to monitor delivery rates, costs, and specific message statuses.
- Alerting: Configure alerts in your monitoring/logging system based on thresholds (e.g., high error rate on the
/api/broadcast/sms
endpoint, high request latency, low delivery rate reported via webhooks).
11. Troubleshooting and Caveats
- Common MessageBird Errors (Check
error.details
from service):AUTHENTICATION_FAILED
(Code 2): Invalid API Key (MESSAGEBIRD_API_KEY
). Check.env
and dashboard.MISSING_PARAMS
/INVALID_PARAMS
(Code 9): Missing required fields (originator
,recipients
,body
) or invalid format (e.g., bad phone number E.164, originator invalid). Check validation logic.- `N