Frequently Asked Questions
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
Technologies Used:
.env
file, keeping sensitive credentials secure.Prerequisites:
ngrok
installed globally (optional but recommended for local development):npm install -g ngrok
System Architecture:
/batches
endpoint_ specifying the recipient_ message body_ and requesting a delivery report.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.
Initialize Node.js Project: Run
npm init
to create apackage.json
file. You can accept the defaults by pressing Enter repeatedly or customize as needed.(The
-y
flag automatically accepts the defaults).Install Dependencies: We need
express
for the web server_axios
to make HTTP requests to Sinch_ anddotenv
to manage environment variables.Create Project Structure: Set up a basic file structure:
Create
.gitignore
: Create a.gitignore
file in the root directory to prevent committing sensitive information and unnecessary files (likenode_modules
and.env
).Set Up Environment Variables (
.env
): Create a.env
file in the root directory. This file will hold your Sinch credentials and configuration. Never commit this file to version control.How to Obtain Sinch Credentials:
SINCH_SERVICE_PLAN_ID
andSINCH_API_TOKEN
:.env
file.SINCH_NUMBER
:+12025550181
).SINCH_API_REGION
:us
,eu
,ca
,au
,br
. Use the correct subdomain prefix for the API base URL (e.g.,us.sms.api.sinch.com
).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.Code Explanation:
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.sinchClient
): Creates a pre-configuredaxios
instance with the correctbaseURL
(including the Service Plan ID) and authentication headers (Authorization: Bearer YOUR_API_TOKEN
). This simplifies making API calls.sendSms
Function:recipientNumber
andmessageBody
as arguments./batches
endpoint.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.sinchClient.post
to send the request.try...catch
error handling, logging details if the API call fails.batchId
on success, which is Sinch's identifier for this message send operation./send-sms
Endpoint (POST):{ ""to"": ""+1..."", ""message"": ""..."" }
.joi
orexpress-validator
) and sanitization.sendSms
function.202 Accepted
if the request to Sinch was successful (SMS sending is asynchronous) or500 Internal Server Error
if it failed./webhooks/dlr
Endpoint (POST):/webhooks/dlr
must exactly match the Callback URL you configure in the Sinch dashboard.express.json()
middleware to automatically parse the incoming JSON payload from Sinch.status
,batch_id
,recipient
, etc., and update your application's database accordingly.200 OK
response back to Sinch immediately to acknowledge receipt. Processing the DLR should happen asynchronously if it's time-consuming, to avoid timeouts./health
Endpoint (GET): A simple endpoint for monitoring systems to check if the application is running.PORT
. Logs helpful information, including the reminder aboutngrok
for 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:
You 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).
ngrok will display output similar to this:
Copy the
https://
forwarding URL. This is the public URL for your local server.Configure Sinch Dashboard:
https://
ngrok URL you copied, appending the webhook path/webhooks/dlr
. The final URL should look like:https://xxxxxxxx.ngrok.io/webhooks/dlr
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:
node index.js
).ngrok
forwarding to your application's port.Send an SMS via the API: Use a tool like
curl
or Postman to send a POST request to your application's/send-sms
endpoint. Replace+1xxxxxxxxxx
with a real phone number you can check.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
.Check Mobile Phone: The recipient phone number should receive the SMS message shortly.
Check ngrok Console: The terminal running
ngrok
might show incoming POST requests to/webhooks/dlr
as 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:The
status
field will show the final delivery state (e.g.,Delivered
,Failed
,Expired
). If you useddelivery_report: 'per_recipient'
, you might receive intermediate statuses likeDispatched
orQueued
as well.Test Failure Scenario (Optional): Try sending to an invalid or deactivated number to observe a
Failed
status in the DLR callback.5. Error Handling, Logging, and Retries
sendSms
function includes basictry...catch
blocks 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).console.log
. For production, use a structured logger likepino
orwinston
to 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.POST /batches
call fails due to network issues or temporary Sinch problems (5xx errors), implement a retry strategy with exponential backoff using libraries likeaxios-retry
orasync-retry
./webhooks/dlr
endpoint 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
/recipient
has 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):
Data Layer Logic:
sms_messages
withstatus='Pending'
, potentially generating and storing aclient_reference
.sinch_batch_id
received from the API response and setstatus='Sent'
.sinch_batch_id
orclient_reference
from the webhook payload.status
(e.g., 'Delivered', 'Failed'),sinch_status_code
,last_status_update_at
, anderror_message
(if applicable).updated_at
.Use an ORM like Prisma or Sequelize to manage migrations and interact with the database safely.
7. Security Features
ngrok
provides this automatically. Production deployments must have valid SSL/TLS certificates./webhooks/dlr
handler 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).SINCH_API_TOKEN
and other secrets securely in environment variables (.env
locally, managed secrets in deployment). Never hardcode them in your source code. Use.gitignore
to prevent committing.env
./send-sms
endpoint (phone number format, message length) and the/webhooks/dlr
endpoint (expected fields, data types). Use libraries likejoi
orexpress-validator
./send-sms
) and especially the webhook endpoint (/webhooks/dlr
) to prevent abuse or accidental overload. Use libraries likeexpress-rate-limit
.npm audit
,npm update
) to patch known vulnerabilities.8. Handling Special Cases
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.batches
API has parameters liketruncate_concat
andmax_number_of_message_parts
if needed.Failed
status with a specific error code. Log these codes for analysis.delivery_report
type requested (full
vs.per_recipient
). Log the full payload during development to understand the structure you receive.9. Performance Optimizations
sinch_batch_id
,client_reference
,status
).k6
,artillery
, orautocannon
to test how your/send-sms
endpoint and/webhooks/dlr
handler perform under load. Identify and address bottlenecks.10. Monitoring, Observability, and Analytics
/health
endpoint provides a basic check. Production systems need more comprehensive health checks that verify database connectivity and potentially Sinch API reachability.prom-client
(for Prometheus):/send-sms
calls)./webhooks/dlr
calls)./batches
).Delivered
,Failed
,Expired
).11. Troubleshooting and Caveats
/webhooks/dlr
path, and points to your publicly accessible server (or ngrok tunnel).delivery_report
Parameter: Verify you are sendingdelivery_report: 'full'
ordelivery_report: 'per_recipient'
in your/batches
request payload.'none'
or'summary'
won't provide detailed status callbacks.http://127.0.0.1:4040
by default) for request logs and errors.Delivered
status usually means delivered to the handset, but edge cases exist.Failed
might have specific error codes indicating the reason (invalid number, blocked, etc.).delivery_report
Type: Ensure you're using the appropriate type (full
orper_recipient
) for the level of detail you need.SINCH_SERVICE_PLAN_ID
,SINCH_API_TOKEN
,SINCH_NUMBER
, andSINCH_API_REGION
in your.env
file./batches
against the Sinch API documentation. Check recipient number format (E.164).