code examples
code examples
Node.js & Express Guide: Sending SMS and Handling Delivery Status Callbacks with Sinch
A comprehensive guide to building a Node.js application using Express to send SMS messages via the Sinch API and handle delivery status report (DLR) callbacks.
Node.js & Express Guide: Sending SMS and Handling Delivery Status Callbacks with Sinch
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Sinch API and reliably handle delivery status report (DLR) callbacks. Understanding SMS delivery status is crucial for applications requiring confirmation that messages reached the recipient's handset, enabling features like status tracking, analytics, and automated retries.
We will build a simple application that:
- Provides an API endpoint to send an SMS message.
- Configures Sinch to send delivery status updates to our application via webhooks.
- Includes an Express route to receive and process these delivery status callbacks.
- Logs the relevant information for monitoring and debugging.
Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express.js: A minimal and flexible Node.js web application framework used here to create API endpoints and handle incoming webhooks.
- Axios: A promise-based HTTP client for making requests to the Sinch API.
- dotenv: A module to load environment variables from a
.envfile, keeping sensitive credentials secure. - Sinch SMS API: The third-party service used for sending SMS messages.
- ngrok (for development): A tool to expose local web servers to the internet, necessary for receiving Sinch webhooks during development.
Prerequisites:
- A Sinch account with SMS API credentials (Service Plan ID, API Token). (Sign up or log in here)
- A provisioned Sinch phone number.
- Node.js and npm (or yarn) installed on your system.
- Basic familiarity with Node.js, Express, and REST APIs.
ngrokinstalled globally (optional but recommended for local development):npm install -g ngrok
System Architecture:
+-----------------+ 1. Send SMS Request +------------+ 2. Send SMS +-----------+
| Your Node.js | ----------------------------> | Sinch API | -------------------> | Recipient |
| Application | (POST /v1/batches) | | | (Phone) |
| (Express Server)| | | +-----------+
| | 4. Delivery Status | | ^
| | <---------------------------- | | | 3. Carrier
| | (POST /webhooks/dlr) | | | Delivery Status
+-----------------+ +------------+ v
+-----------+
| Carrier |
+-----------+
- Your application sends an SMS request to the Sinch API
/batchesendpoint, specifying the recipient, message body, and requesting a delivery report. - Sinch processes the request and sends the SMS towards the recipient's carrier.
- The carrier attempts delivery and sends status updates back to Sinch.
- Sinch forwards the final delivery status (e.g.,
Delivered,Failed) to the callback URL configured in your Sinch account, which points to your application's webhook handler endpoint.
By the end of this guide, you will have a functional Node.js application capable of sending SMS messages and logging their delivery status received via Sinch webhooks.
1. Setting Up the Project
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.
bashmkdir sinch-sms-dlr-app cd sinch-sms-dlr-app -
Initialize Node.js Project: Run
npm initto create apackage.jsonfile. You can accept the defaults by pressing Enter repeatedly or customize as needed.bashnpm init -y(The
-yflag automatically accepts the defaults). -
Install Dependencies: We need
expressfor the web server,axiosto make HTTP requests to Sinch, anddotenvto manage environment variables.bashnpm install express axios dotenv -
Create Project Structure: Set up a basic file structure:
sinch-sms-dlr-app/ ├── .env # Stores environment variables (API keys, etc.) ├── .gitignore # Specifies intentionally untracked files that Git should ignore ├── index.js # Main application file ├── package.json └── node_modules/ # Created by npm install -
Create
.gitignore: Create a.gitignorefile in the root directory to prevent committing sensitive information and unnecessary files (likenode_modulesand.env).plaintext# .gitignore node_modules/ .env npm-debug.log* yarn-debug.log* yarn-error.log* -
Set Up Environment Variables (
.env): Create a.envfile in the root directory. This file will hold your Sinch credentials and configuration. Never commit this file to version control.plaintext# .env # Sinch API Credentials SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID SINCH_API_TOKEN=YOUR_API_TOKEN SINCH_NUMBER=YOUR_SINCH_VIRTUAL_NUMBER # e.g., +12025550181 SINCH_API_REGION=us # Or eu, ca, au, br, etc. based on your account region # Application Configuration PORT=3000How to Obtain Sinch Credentials:
SINCH_SERVICE_PLAN_IDandSINCH_API_TOKEN:- Log in to your Sinch Customer Dashboard (https://dashboard.sinch.com/login).
- Navigate to SMS > APIs in the left-hand menu.
- Your Service plan ID is listed here.
- Under API Credentials, find your API token. Click Show to reveal it.
- Copy both values into your
.envfile.
SINCH_NUMBER:- In the Sinch Dashboard, navigate to Numbers > Your virtual numbers.
- Copy one of your active Sinch numbers capable of sending SMS (ensure it's in E.164 format, e.g.,
+12025550181).
SINCH_API_REGION:- Check the region associated with your Service Plan ID in the SMS > APIs section of the dashboard. Common values are
us,eu,ca,au,br. Use the correct subdomain prefix for the API base URL (e.g.,us.sms.api.sinch.com).
- Check the region associated with your Service Plan ID in the SMS > APIs section of the dashboard. Common values are
2. Implementing Core Functionality
Now, let's write the code in index.js to set up the Express server, send SMS messages, and handle incoming delivery report webhooks.
// index.js
require('dotenv').config(); // Load environment variables from .env file
const express = require('express');
const axios = require('axios');
const app = express();
// Middleware to parse JSON request bodies
// Sinch sends webhooks as JSON
app.use(express.json());
// --- Configuration ---
const PORT = process.env.PORT || 3000;
const SINCH_SERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID;
const SINCH_API_TOKEN = process.env.SINCH_API_TOKEN;
const SINCH_NUMBER = process.env.SINCH_NUMBER;
const SINCH_API_REGION = process.env.SINCH_API_REGION || 'us'; // Default to 'us' if not set
const SINCH_API_BASE_URL = `https://${SINCH_API_REGION}.sms.api.sinch.com/xms/v1/`;
// Basic validation to ensure credentials are loaded
if (!SINCH_SERVICE_PLAN_ID || !SINCH_API_TOKEN || !SINCH_NUMBER) {
console.error(""Error: Missing Sinch credentials in .env file. Please check your configuration."");
process.exit(1); // Exit if critical configuration is missing
}
// --- Sinch API Client Setup ---
const sinchClient = axios.create({
baseURL: SINCH_API_BASE_URL + SINCH_SERVICE_PLAN_ID,
headers: {
'Authorization': `Bearer ${SINCH_API_TOKEN}`,
'Content-Type': 'application/json'
}
});
// --- Function to Send SMS ---
async function sendSms(recipientNumber, messageBody) {
console.log(`Attempting to send SMS to ${recipientNumber}`);
try {
const payload = {
from: SINCH_NUMBER,
to: [recipientNumber], // Must be an array
body: messageBody,
// Request a detailed delivery report for each recipient status change
// Options: 'none', 'summary', 'full', 'per_recipient'
// 'full' or 'per_recipient' are best for detailed tracking.
// 'full' sends one callback per batch status change.
// 'per_recipient' sends a callback for *each* recipient's status change (can be noisy).
// We use 'full' here for simplicity, but often 'per_recipient' is needed.
delivery_report: 'full',
// Optional: You can override the global callback URL per message
// callback_url: 'YOUR_SPECIFIC_CALLBACK_URL_HERE'
// Optional: Client reference for correlating DLRs
// client_reference: `my_internal_id_${Date.now()}`
};
const response = await sinchClient.post('/batches', payload);
console.log('Sinch API Send Request Successful:');
console.log(`Batch ID: ${response.data.id}`);
console.log(`Sent to: ${response.data.to.join(', ')}`);
// Return the batch ID for potential future tracking
return { success: true, batchId: response.data.id };
} catch (error) {
console.error('Error sending SMS via Sinch API:');
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error('Status:', error.response.status);
console.error('Headers:', error.response.headers);
console.error('Data:', error.response.data);
} else if (error.request) {
// The request was made but no response was received
console.error('Request Error:', error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error Message:', error.message);
}
return { success: false, error: error.message };
}
}
// --- API Endpoint to Trigger SMS Sending ---
// Example: POST /send-sms with JSON body: { ""to"": ""+15551234567"", ""message"": ""Hello from Sinch!"" }
app.post('/send-sms', async (req, res) => {
const { to, message } = req.body;
// Basic Input Validation
if (!to || !message) {
return res.status(400).json({ error: 'Missing ""to"" or ""message"" in request body' });
}
// Add more robust validation (e.g., phone number format) in production
if (!/^\+[1-9]\d{1,14}$/.test(to)) {
return res.status(400).json({ error: 'Invalid ""to"" phone number format. Use E.164 format (e.g., +15551234567).' });
}
const result = await sendSms(to, message);
if (result.success) {
res.status(202).json({ message: 'SMS send request accepted by Sinch.', batchId: result.batchId });
} else {
res.status(500).json({ error: 'Failed to send SMS.', details: result.error });
}
});
// --- Webhook Endpoint for Delivery Reports (DLRs) ---
// Sinch will POST delivery status updates to this endpoint
// The exact path '/webhooks/dlr' should match the callback URL configured in Sinch
app.post('/webhooks/dlr', (req, res) => {
console.log('--- Received Sinch Delivery Report ---');
console.log('Timestamp:', new Date().toISOString());
console.log('Request Body:', JSON.stringify(req.body, null, 2)); // Pretty print the JSON
// Structure of the DLR payload can vary based on 'delivery_report' setting and event type.
// Check Sinch documentation for the exact format based on your 'delivery_report' setting.
// Example for 'full' or 'per_recipient':
const { batch_id, status, recipient, client_reference, applied_originator } = req.body;
// Process the DLR - In a real application, you would:
// 1. Validate the request (e.g., check a signature if provided by Sinch).
// 2. Look up the message/batch in your database using batch_id or client_reference.
// 3. Update the message status in your database (e.g., 'DELIVERED', 'FAILED', etc.).
// 4. Trigger any follow-up actions based on the status.
console.log(`Status for Batch ID [${batch_id}] to Recipient [${recipient || 'N/A'}]: ${status}`);
if (client_reference) {
console.log(`Client Reference: ${client_reference}`);
}
// Always respond to Sinch with a 200 OK quickly to acknowledge receipt.
// Failure to respond promptly may cause Sinch to retry the webhook.
res.status(200).send('OK');
});
// --- Basic Health Check Endpoint ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// --- Start the Server ---
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`Sinch API endpoint: ${SINCH_API_BASE_URL}${SINCH_SERVICE_PLAN_ID}`);
console.log(`Configured Sinch Number: ${SINCH_NUMBER}`);
console.log('Waiting for incoming requests and webhooks...');
console.log('\n--- IMPORTANT ---');
console.log('For local development, ensure ngrok is running and');
console.log('the ngrok HTTPS URL is configured as the Callback URL');
console.log('in your Sinch dashboard for the Service Plan ID.');
console.log('Example ngrok command: ngrok http 3000');
console.log('Example Callback URL: https://your-unique-ngrok-id.ngrok.io/webhooks/dlr');
console.log('--------------- \n');
});Code Explanation:
- Dependencies & Config: Loads
dotenv,express,axios. Sets up constants for port and Sinch credentials loaded from.env. Includes basic validation to ensure credentials exist. Constructs the base URL for the Sinch API based on the region. - Axios Instance (
sinchClient): Creates a pre-configuredaxiosinstance with the correctbaseURL(including the Service Plan ID) and authentication headers (Authorization: Bearer YOUR_API_TOKEN). This simplifies making API calls. sendSmsFunction:- Takes the
recipientNumberandmessageBodyas arguments. - Constructs the payload for the Sinch
/batchesendpoint.from: Your Sinch virtual number.to: An array containing the recipient's phone number (E.164 format).body: The text message content.delivery_report: 'full': Crucially, this tells Sinch to send a comprehensive delivery status report back via webhook. You could also use'per_recipient'for even more granular (but potentially noisier) updates.'none'or'summary'won't provide the detailed status needed.client_reference(Optional but Recommended): Include a unique identifier from your system. This ID will be included in the DLR webhook, making it easier to correlate the status update back to the original message in your database.
- Uses
sinchClient.postto send the request. - Includes robust
try...catcherror handling, logging details if the API call fails. - Returns the
batchIdon success, which is Sinch's identifier for this message send operation.
- Takes the
/send-smsEndpoint (POST):- Defines a route to trigger sending an SMS. It expects a JSON body like
{ ""to"": ""+1..."", ""message"": ""..."" }. - Performs basic validation on the input. In production, add more robust validation (e.g., using a library like
joiorexpress-validator) and sanitization. - Calls the
sendSmsfunction. - Responds with
202 Acceptedif the request to Sinch was successful (SMS sending is asynchronous) or500 Internal Server Errorif it failed.
- Defines a route to trigger sending an SMS. It expects a JSON body like
/webhooks/dlrEndpoint (POST):- This is the endpoint Sinch will call with delivery status updates. The path
/webhooks/dlrmust exactly match the Callback URL you configure in the Sinch dashboard. - Uses
express.json()middleware to automatically parse the incoming JSON payload from Sinch. - Logs the received DLR payload. In a real application, this is where you'd parse the
status,batch_id,recipient, etc., and update your application's database accordingly. - Sends a
200 OKresponse back to Sinch immediately to acknowledge receipt. Processing the DLR should happen asynchronously if it's time-consuming, to avoid timeouts.
- This is the endpoint Sinch will call with delivery status updates. The path
/healthEndpoint (GET): A simple endpoint for monitoring systems to check if the application is running.- Server Start: Starts the Express server, listening on the configured
PORT. Logs helpful information, including the reminder aboutngrokfor local development.
3. Configuring Sinch Callback URL
For Sinch to send delivery reports back to your running application, you need to configure a callback URL in your Sinch dashboard. During local development, this requires exposing your local server to the internet.
-
Start Your Node.js Application:
bashnode index.jsYou should see the server start message and the prompt about ngrok.
-
Start ngrok: Open another terminal window and run ngrok, pointing it to the port your application is running on (default is 3000).
bashngrok http 3000ngrok will display output similar to this:
Session Status online Account Your Name (Plan: Free) Version x.x.x Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://xxxxxxxx.ngrok.io -> http://localhost:3000 Forwarding https://xxxxxxxx.ngrok.io -> http://localhost:3000 # <-- COPY THIS HTTPS URL Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00Copy the
https://forwarding URL. This is the public URL for your local server. -
Configure Sinch Dashboard:
- Go to your Sinch Customer Dashboard.
- Navigate to SMS > APIs.
- Click on your Service Plan ID.
- Scroll down to the Callback URLs section.
- Find the Default callback URL for delivery reports field (or similar wording for delivery report callbacks).
- Paste the
https://ngrok URL you copied, appending the webhook path/webhooks/dlr. The final URL should look like:https://xxxxxxxx.ngrok.io/webhooks/dlr - Click Save.
- (In a production environment, you would use the stable public URL of your deployed application instead of an ngrok URL.)
Now, when Sinch has a delivery status update for an SMS sent using this Service Plan ID (and where delivery_report was requested), it will send a POST request to your ngrok URL, which will forward it to your local application's /webhooks/dlr endpoint.
4. Verification and Testing
Let's test the entire flow:
-
Ensure Both Processes Are Running:
- Your Node.js application (
node index.js). ngrokforwarding to your application's port.
- Your Node.js application (
-
Send an SMS via the API: Use a tool like
curlor Postman to send a POST request to your application's/send-smsendpoint. Replace+1xxxxxxxxxxwith a real phone number you can check.bashcurl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+1xxxxxxxxxx"", ""message"": ""Hello from Node/Sinch! Testing DLR. [Timestamp: '""$(date)""']"" }' -
Check Application Logs (Send Request): In the terminal running
node index.js, you should see logs indicating the attempt to send the SMS and the response from the Sinch API, including thebatchId.Attempting to send SMS to +1xxxxxxxxxx Sinch API Send Request Successful: Batch ID: 01HXXXXXXXXXXXXXXEXAMPLEID Sent to: +1xxxxxxxxxx -
Check Mobile Phone: The recipient phone number should receive the SMS message shortly.
-
Check ngrok Console: The terminal running
ngrokmight show incoming POST requests to/webhooks/dlras Sinch sends status updates. -
Check Application Logs (DLR Callback): Wait a few seconds or minutes (delivery time varies). In the terminal running
node index.js, you should see logs indicating a received delivery report webhook:--- Received Sinch Delivery Report --- Timestamp: 2023-04-20T10:30:00.123Z Request Body: { ""batch_id"": ""01HXXXXXXXXXXXXXXEXAMPLEID"", ""status"": ""Delivered"", ""recipient"": ""+1xxxxxxxxxx"", ""operator_status_at"": ""2023-04-20T10:30:00.000Z"", ""type"": ""delivery_report_sms"", ""code"": 0, ""client_reference"": null // Or your reference if you sent one // ... other potential fields } Status for Batch ID [01HXXXXXXXXXXXXXXEXAMPLEID] to Recipient [+1xxxxxxxxxx]: DeliveredThe
statusfield will show the final delivery state (e.g.,Delivered,Failed,Expired). If you useddelivery_report: 'per_recipient', you might receive intermediate statuses likeDispatchedorQueuedas well. -
Test Failure Scenario (Optional): Try sending to an invalid or deactivated number to observe a
Failedstatus in the DLR callback.
5. Error Handling, Logging, and Retries
- Error Handling: The
sendSmsfunction includes basictry...catchblocks logging detailed errors fromaxios(status codes, response data). The webhook handler logs the incoming payload. Production applications need more sophisticated error tracking (e.g., Sentry, Datadog). - Logging: We use
console.log. For production, use a structured logger likepinoorwinstonto output JSON logs, making them easier to parse and analyze. Log key events: SMS request received, API call attempt, API response (success/failure), DLR received, DLR processing result. Include correlation IDs (batch_id,client_reference) in logs. - Retry Mechanisms (API Calls): If the initial
POST /batchescall fails due to network issues or temporary Sinch problems (5xx errors), implement a retry strategy with exponential backoff using libraries likeaxios-retryorasync-retry. - Retry Mechanisms (Webhooks): Sinch will automatically retry sending webhooks if your endpoint doesn't respond with a 2xx status code within a certain timeout. Ensure your
/webhooks/dlrendpoint responds quickly (200 OK) and handles processing potentially asynchronously (e.g., pushing the DLR payload to a queue like Redis or RabbitMQ for later processing) to avoid causing unnecessary retries from Sinch. Make your webhook handler idempotent – processing the same DLR multiple times should not cause issues (e.g., check if the status for thatbatch_id/recipienthas already been updated).
6. Database Schema and Data Layer (Conceptual)
While this guide doesn't implement a database, a real-world application would need one to store message information and track status.
Conceptual Schema (e.g., PostgreSQL):
CREATE TABLE sms_messages (
message_id SERIAL PRIMARY KEY, -- Internal unique ID
sinch_batch_id VARCHAR(255) UNIQUE, -- Batch ID from Sinch API response
client_reference VARCHAR(255) UNIQUE, -- Optional: Your internal reference sent to Sinch
recipient_number VARCHAR(20) NOT NULL,
sender_number VARCHAR(20) NOT NULL,
message_body TEXT,
status VARCHAR(50) DEFAULT 'Pending', -- e.g., Pending, Sent, Delivered, Failed, Expired
send_requested_at TIMESTAMPTZ DEFAULT NOW(),
last_status_update_at TIMESTAMPTZ,
sinch_status_code INT, -- Status code from DLR
error_message TEXT, -- Store failure reasons
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for efficient lookup by Sinch identifiers
CREATE INDEX idx_sms_sinch_batch_id ON sms_messages(sinch_batch_id);
CREATE INDEX idx_sms_client_reference ON sms_messages(client_reference);
CREATE INDEX idx_sms_status ON sms_messages(status);Data Layer Logic:
- Before Sending: Create a record in
sms_messageswithstatus='Pending', potentially generating and storing aclient_reference. - After Sending (API Success): Update the record with the
sinch_batch_idreceived from the API response and setstatus='Sent'. - On DLR Webhook:
- Look up the message using
sinch_batch_idorclient_referencefrom the webhook payload. - Update the
status(e.g., 'Delivered', 'Failed'),sinch_status_code,last_status_update_at, anderror_message(if applicable). - Update
updated_at.
- Look up the message using
Use an ORM like Prisma or Sequelize to manage migrations and interact with the database safely.
7. Security Features
- Webhook Security:
- HTTPS: Always use HTTPS for your callback URL.
ngrokprovides this automatically. Production deployments must have valid SSL/TLS certificates. - Secret/Signature Verification (Recommended): Check if Sinch supports sending a signature (e.g., HMAC-SHA256) in the webhook request header based on a shared secret. If so, configure a secret in the dashboard and verify the signature in your
/webhooks/dlrhandler before processing. This ensures the request genuinely came from Sinch. If Sinch doesn't offer this for SMS DLRs, consider adding a unique, hard-to-guess path or a secret query parameter to your callback URL (less secure but better than nothing). - IP Whitelisting: If Sinch publishes the IP addresses used for sending webhooks, configure your firewall or infrastructure to only allow requests to your webhook endpoint from those IPs.
- HTTPS: Always use HTTPS for your callback URL.
- API Key Security: Store
SINCH_API_TOKENand other secrets securely in environment variables (.envlocally, managed secrets in deployment). Never hardcode them in your source code. Use.gitignoreto prevent committing.env. - Input Validation: Rigorously validate all incoming data, especially in the
/send-smsendpoint (phone number format, message length) and the/webhooks/dlrendpoint (expected fields, data types). Use libraries likejoiorexpress-validator. - Rate Limiting: Implement rate limiting on your API endpoints (
/send-sms) and especially the webhook endpoint (/webhooks/dlr) to prevent abuse or accidental overload. Use libraries likeexpress-rate-limit. - Dependencies: Keep dependencies updated (
npm audit,npm update) to patch known vulnerabilities.
8. Handling Special Cases
- Time Zones: Timestamps from Sinch (e.g.,
operator_status_at) are typically in UTC (ISO 8601 format). Store timestamps in your database usingTIMESTAMPTZ(Timestamp with Time Zone) in PostgreSQL or equivalent, which usually stores in UTC and handles conversions. Be mindful of time zone conversions when displaying times to users. - Character Encoding/Limits: Standard SMS messages have character limits (160 for GSM-7, 70 for UCS-2). Longer messages are split into multiple parts (concatenated SMS). Sinch handles this splitting, but it affects billing. Be aware of how special characters might force UCS-2 encoding, reducing the characters per part. The
batchesAPI has parameters liketruncate_concatandmax_number_of_message_partsif needed. - Invalid Numbers: DLRs for invalid numbers will typically result in a
Failedstatus with a specific error code. Log these codes for analysis. - Opt-Outs/STOP Keywords: Sinch often handles standard STOP keyword replies automatically if configured. Ensure you respect opt-outs and don't send further messages to users who have replied STOP. DLRs might indicate failure if sent to an opted-out number.
- DLR Delays: Delivery reports are not always instantaneous. They depend on carrier reporting speed. Your application should handle potential delays gracefully. Status might remain 'Sent' for some time before updating.
- DLR Format Variations: The exact fields in the DLR payload can sometimes vary slightly depending on the carrier, region, or the specific
delivery_reporttype requested (fullvs.per_recipient). Log the full payload during development to understand the structure you receive.
9. Performance Optimizations
- Asynchronous Processing: Process DLR webhooks asynchronously. The main handler should quickly validate (if possible), acknowledge (200 OK), and then pass the payload to a background job queue (e.g., BullMQ with Redis, RabbitMQ) for database updates and other logic. This prevents blocking the event loop and avoids webhook timeouts/retries from Sinch.
- Database Indexing: Ensure proper database indexes (as shown in section 6) on fields used for lookups (
sinch_batch_id,client_reference,status). - Connection Pooling: Use database connection pooling (typically handled by ORMs like Prisma/Sequelize) to reuse database connections efficiently.
- Load Testing: Use tools like
k6,artillery, orautocannonto test how your/send-smsendpoint and/webhooks/dlrhandler perform under load. Identify and address bottlenecks. - Caching: If you frequently query the status of recently sent messages, consider caching status information (e.g., in Redis) for a short duration to reduce database load, but be mindful of cache invalidation when a new DLR arrives.
10. Monitoring, Observability, and Analytics
- Health Checks: The
/healthendpoint provides a basic check. Production systems need more comprehensive health checks that verify database connectivity and potentially Sinch API reachability. - Metrics: Instrument your application to collect key metrics using libraries like
prom-client(for Prometheus):- Rate of outgoing SMS requests (
/send-smscalls). - Rate of incoming DLR webhooks (
/webhooks/dlrcalls). - Latency of Sinch API calls (
/batches). - Histogram of DLR processing times.
- Counts of successful vs. failed SMS sends (based on DLRs).
- Counts of different DLR statuses (
Delivered,Failed,Expired). - Application error rates.
- Rate of outgoing SMS requests (
- Logging: Centralized, structured logging (ELK stack, Grafana Loki, Datadog Logs) is essential for debugging and analysis. Include correlation IDs.
- Error Tracking: Integrate services like Sentry or Datadog APM to automatically capture and aggregate application errors with stack traces and context.
- Dashboards: Create dashboards (e.g., in Grafana, Datadog) visualizing the metrics collected above to monitor system health and SMS delivery performance in real-time.
- Alerting: Configure alerts based on metrics (e.g., high API error rate, high SMS failure rate, DLR webhook processing latency exceeding threshold, health check failures) using Prometheus Alertmanager, Grafana alerting, or your monitoring platform's tools.
11. Troubleshooting and Caveats
- DLRs Not Received:
- Check Callback URL: Ensure the URL in the Sinch dashboard is correct, uses HTTPS, includes the
/webhooks/dlrpath, and points to your publicly accessible server (or ngrok tunnel). - Check
delivery_reportParameter: Verify you are sendingdelivery_report: 'full'ordelivery_report: 'per_recipient'in your/batchesrequest payload.'none'or'summary'won't provide detailed status callbacks. - Check Server/Firewall: Ensure your server is running and accessible from the internet. Check firewall rules aren't blocking incoming POST requests from Sinch IPs (if known/used).
- Check Application Logs: Look for errors in your webhook handler that might cause it to crash or return non-200 status codes.
- Check Sinch Logs: Investigate logs within the Sinch dashboard (if available) for errors related to callback attempts.
- Check ngrok: Ensure ngrok is running and hasn't timed out (free plans have limits). Check the ngrok web interface (
http://127.0.0.1:4040by default) for request logs and errors.
- Check Callback URL: Ensure the URL in the Sinch dashboard is correct, uses HTTPS, includes the
- Incorrect DLR Status:
- Carrier Limitations: Some carriers provide limited or delayed delivery information. A
Deliveredstatus usually means delivered to the handset, but edge cases exist.Failedmight have specific error codes indicating the reason (invalid number, blocked, etc.). - Check
delivery_reportType: Ensure you're using the appropriate type (fullorper_recipient) for the level of detail you need.
- Carrier Limitations: Some carriers provide limited or delayed delivery information. A
- Webhook Handler Issues:
- Timeouts: If your handler takes too long to process, Sinch might retry, leading to duplicate processing. Acknowledge quickly (200 OK) and process asynchronously.
- Idempotency: Design your handler to safely process the same DLR multiple times without side effects.
- Parsing Errors: Ensure your code correctly parses the JSON payload from Sinch. Log the raw body on error.
- API Call Failures:
- Credentials: Double-check
SINCH_SERVICE_PLAN_ID,SINCH_API_TOKEN,SINCH_NUMBER, andSINCH_API_REGIONin your.envfile. - Rate Limits: Check if you are exceeding Sinch API rate limits.
- Network Issues: Ensure your server has outbound connectivity to the Sinch API endpoint.
- Invalid Payload: Validate the structure and content of the payload sent to
/batchesagainst the Sinch API documentation. Check recipient number format (E.164).
- Credentials: Double-check
Frequently Asked Questions
Why am I not receiving Sinch DLR callbacks?
Possible reasons include incorrect callback URL configuration in the Sinch dashboard, forgetting to set 'delivery_report' in the send request, server or firewall issues, or errors in the webhook handler code. Check logs in your application, ngrok, and the Sinch dashboard for clues.
How to send SMS messages with Sinch and Node.js?
Use the Sinch SMS API with a Node.js library like Axios to send messages. Create a request payload including the recipient number, message body, your Sinch number, and set 'delivery_report' to 'full' or 'per_recipient' to receive delivery status updates via webhooks. Send the POST request to the /batches endpoint of the Sinch API using your Service Plan ID and API token for authentication.
What is a delivery status report (DLR) callback with Sinch?
A DLR callback is a notification Sinch sends to your application's webhook URL when the status of a sent SMS message changes (e.g., delivered, failed). This provides real-time feedback on message delivery outcomes.
Why use ngrok with Sinch SMS API during development?
ngrok creates a public, secure tunnel to your locally running application, allowing Sinch to send DLR webhooks to your local development environment. It's essential for testing the callback functionality without deploying your application.
When should I use 'full' vs. 'per_recipient' for Sinch delivery reports?
Use 'full' for a summary of delivery status changes per message batch, suitable for basic tracking. 'per_recipient' sends individual callbacks for each recipient's status change (more detailed but potentially noisier), which is often preferred for granular tracking and analysis.
Can I handle Sinch DLR webhooks in Express.js?
Yes, create a POST route in your Express app that matches the callback URL configured in your Sinch account. The route handler should parse the JSON payload, log the status, and update your internal systems based on the reported message status (e.g., delivered, failed). Ensure the endpoint responds quickly with 200 OK.
How to set up Sinch API credentials in a Node.js project?
Store your Sinch Service Plan ID, API Token, Sinch virtual number, and region in a .env file. Load these environment variables into your Node.js application at runtime using the 'dotenv' package. Never hardcode API credentials directly in your source code, as it poses security risks.
What is the purpose of the 'client_reference' field in Sinch API requests?
The optional 'client_reference' field lets you send a custom identifier (e.g., your internal message ID) along with the SMS request. Sinch includes this reference in the DLR callback, allowing you to easily link the status update to the original message in your database.
How to configure Sinch callback URL for delivery reports?
Log into your Sinch Dashboard, navigate to SMS > APIs > Your Service Plan ID. Under 'Callback URLs', enter the HTTPS URL of your application's webhook endpoint, which is usually your server address plus '/webhooks/dlr', e.g., https://your-server.com/webhooks/dlr or https://your-ngrok-id.ngrok.io/webhooks/dlr.
What does 'status: Delivered' mean in a Sinch DLR callback?
The 'Delivered' status typically indicates successful delivery of the SMS message to the recipient's handset. However, there can be carrier-specific nuances, and it's not always a 100% guarantee of being read by the recipient.
How to handle DLR callbacks asynchronously in Node.js?
Use a queue system like Redis, RabbitMQ, or BullMQ. Your webhook handler should quickly acknowledge receipt of the DLR and then place the DLR data onto the queue for background processing. This improves responsiveness and prevents blocking the main thread.
What are best practices for Sinch webhook security?
Always use HTTPS. If Sinch supports it, implement signature verification using a shared secret to authenticate webhooks. Consider IP whitelisting if Sinch provides its outgoing IP addresses. In any case, rate-limit your webhook endpoint to mitigate abuse.
How to handle SMS message failures reported by Sinch DLR?
Log the failure reason and status code from the DLR. Update your application's message status accordingly. Implement retry mechanisms with exponential backoff, but respect potential opt-outs or blocks to avoid excessive sending attempts. Notify administrators or users about critical failures.
How should I design my database schema for storing SMS messages and DLR status?
Create a table with columns for a unique message ID, Sinch batch ID, client reference, recipient, sender, message body, status, timestamps, error messages, and any other relevant metadata. Index the Sinch batch ID and client reference for fast lookups.
What are some performance tips for handling large volumes of SMS messages and DLRs?
Process DLRs asynchronously. Use database indexing for efficient lookups. Implement connection pooling for database interactions. Load test your application to identify bottlenecks. Consider caching status information (with appropriate invalidation) for frequently accessed data.