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:
.envfile, keeping sensitive credentials secure.Prerequisites:
ngrokinstalled globally (optional but recommended for local development):npm install -g ngrokSystem Architecture:
/batchesendpoint_ 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 initto create apackage.jsonfile. You can accept the defaults by pressing Enter repeatedly or customize as needed.(The
-yflag automatically accepts the defaults).Install Dependencies: We need
expressfor the web server_axiosto make HTTP requests to Sinch_ anddotenvto manage environment variables.Create Project Structure: Set up a basic file structure:
Create
.gitignore: Create a.gitignorefile in the root directory to prevent committing sensitive information and unnecessary files (likenode_modulesand.env).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.How to Obtain Sinch Credentials:
SINCH_SERVICE_PLAN_IDandSINCH_API_TOKEN:.envfile.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.jsto 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-configuredaxiosinstance with the correctbaseURL(including the Service Plan ID) and authentication headers (Authorization: Bearer YOUR_API_TOKEN). This simplifies making API calls.sendSmsFunction:recipientNumberandmessageBodyas arguments./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.sinchClient.postto send the request.try...catcherror handling, logging details if the API call fails.batchIdon success, which is Sinch's identifier for this message send operation./send-smsEndpoint (POST):{ ""to"": ""+1..."", ""message"": ""..."" }.joiorexpress-validator) and sanitization.sendSmsfunction.202 Acceptedif the request to Sinch was successful (SMS sending is asynchronous) or500 Internal Server Errorif it failed./webhooks/dlrEndpoint (POST):/webhooks/dlrmust 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 OKresponse back to Sinch immediately to acknowledge receipt. Processing the DLR should happen asynchronously if it's time-consuming, to avoid timeouts./healthEndpoint (GET): A simple endpoint for monitoring systems to check if the application is running.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:
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/dlrNow, when Sinch has a delivery status update for an SMS sent using this Service Plan ID (and where
delivery_reportwas requested), it will send a POST request to your ngrok URL, which will forward it to your local application's/webhooks/dlrendpoint.4. Verification and Testing
Let's test the entire flow:
Ensure Both Processes Are Running:
node index.js).ngrokforwarding to your application's port.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.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
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:The
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
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).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.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./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):
Data Layer Logic:
sms_messageswithstatus='Pending', potentially generating and storing aclient_reference.sinch_batch_idreceived from the API response and setstatus='Sent'.sinch_batch_idorclient_referencefrom 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
ngrokprovides this automatically. Production deployments must have valid SSL/TLS certificates./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).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./send-smsendpoint (phone number format, message length) and the/webhooks/dlrendpoint (expected fields, data types). Use libraries likejoiorexpress-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.batchesAPI has parameters liketruncate_concatandmax_number_of_message_partsif needed.Failedstatus with a specific error code. Log these codes for analysis.delivery_reporttype requested (fullvs.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, orautocannonto test how your/send-smsendpoint and/webhooks/dlrhandler perform under load. Identify and address bottlenecks.10. Monitoring, Observability, and Analytics
/healthendpoint 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-smscalls)./webhooks/dlrcalls)./batches).Delivered,Failed,Expired).11. Troubleshooting and Caveats
/webhooks/dlrpath, and points to your publicly accessible server (or ngrok tunnel).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.http://127.0.0.1:4040by default) for request logs and errors.Deliveredstatus usually means delivered to the handset, but edge cases exist.Failedmight have specific error codes indicating the reason (invalid number, blocked, etc.).delivery_reportType: Ensure you're using the appropriate type (fullorper_recipient) for the level of detail you need.SINCH_SERVICE_PLAN_ID,SINCH_API_TOKEN,SINCH_NUMBER, andSINCH_API_REGIONin your.envfile./batchesagainst the Sinch API documentation. Check recipient number format (E.164).