Frequently Asked Questions
Implement Sinch SMS callbacks by creating a Node.js/Express backend with a secure webhook endpoint. This endpoint receives delivery status updates from Sinch. Use the Sinch Node.js SDK to send SMS messages and the express.raw middleware to handle incoming raw callback data for proper HMAC verification before parsing the JSON payload.
HMAC validation ensures the authenticity and integrity of Sinch webhook callbacks. It confirms that the received data originates from Sinch and hasn't been tampered with, adding a crucial security layer to your application.
The express.raw middleware is essential for receiving the raw, unparsed request body containing callback data from Sinch. This is required for accurate HMAC signature validation, which must be done before any JSON parsing.
In the Sinch Dashboard, navigate to the API settings for your SMS service. Create a new webhook, specifying the target URL (your backend endpoint exposed via ngrok during development), HTTP target type, a strong secret (matching your server's SINCH_WEBHOOK_SECRET), and the MESSAGE_DELIVERY trigger. Remember to link the webhook to the relevant service or app ID if necessary.
The Sinch SMS callback payload, particularly the message_delivery_report, includes the message_id (matching the sent batch ID), delivery status (e.g., DELIVERED, FAILED), recipient number, optional failure reason, and the event_time timestamp.
Store delivery status updates in a database after validating the callback's HMAC signature. Log the message_id, status, reason, and event_time from the callback payload. In a production environment, establish a connection to your database and update records based on the message_id.
ngrok creates a public tunnel to your locally running backend server. This allows Sinch to send webhooks to your development environment during testing, as Sinch needs a publicly accessible URL to send callbacks to.
Use ngrok during local development and testing when your backend server isn't publicly accessible. This enables Sinch to deliver callbacks to your localhost for testing purposes. In production, your backend should be deployed publicly, and ngrok isn't needed.
Use the Sinch Node.js SDK's sinchClient.sms.batches.send method. Provide the recipient number(s), sender number (SINCH_NUMBER), message body, and any optional parameters (like delivery_report). This sends the SMS via the Sinch API.
The Sinch batch ID or messageId is a unique identifier for each batch of SMS messages sent. This ID is crucial for correlating delivery reports received via webhooks back to the originally sent message(s). It's typically included in the callback payload and initial send response.
Implement try...catch blocks around Sinch API calls to handle potential errors. Log detailed error information on the server-side using console.error. Return user-friendly error messages to the frontend without revealing sensitive details.
A recommended project structure includes a 'client' directory for the frontend (e.g., Vite/React) and a 'server' directory for the Node.js/Express backend. Key files in the backend include server.js (main server logic), sinchClient.js (Sinch SDK setup), and .env (environment variables).
Sinch SMS Delivery Status Callbacks in Node.js: Complete Guide for Vite Apps
Learn how to implement Sinch SMS delivery status callbacks using Node.js, Express, and Vite for real-time message tracking. This comprehensive guide covers building a secure webhook endpoint that receives delivery reports from the Sinch API, validating callbacks with HMAC signatures, and integrating status tracking with your React frontend application.
Important: SDK Package Verification
This guide uses
@sinch/sdk-corefor the Sinch Node.js SDK. As of 2024-2025, Sinch offers multiple SDK packages. Verify the current recommended package at Sinch Developer Documentation before implementation. Alternative packages may include@sinch/sdk-smsor platform-specific SDKs. The authentication and API methods shown may vary by package version.Building an SMS Delivery Tracking System with Sinch Webhooks
System Components:
You'll create a system comprising:
messageId(batch ID) received after sending.Problem Solved:
Your applications need real-time visibility into SMS message delivery status to know if messages successfully reach recipients' handsets. Relying solely on the initial API response isn't enough – delivery is asynchronous. Sinch provides delivery status callbacks via webhooks, enabling you to track message status changes from queued to delivered or failed. Implementing a secure and reliable webhook endpoint requires careful setup with proper HMAC signature validation. This guide shows you how to:
Technologies Used:
@sinch/sdk-core): Official SDK for interacting with Sinch APIs. (Note: Verify against current Sinch documentation if this core package is sufficient or if a more specific SMS package is recommended for your use case).dotenv: Module to load environment variables from a.envfile.cors: Express middleware to enable Cross-Origin Resource Sharing (necessary for local development).ngrok(for testing): A tool to expose local servers to the internet, allowing Sinch to send callbacks to your development machine.System Architecture:
/send-smsendpoint on the Node.js backend.batch_id(ormessageId).batch_idto the frontend.Prerequisites:
Project ID_Key ID_Key Secret: Found on your Sinch account dashboard. Needed for most APIs including SMS.ngrok(Optional but Recommended for Local Testing): Download ngrokHow to Set Up Your Node.js Project Structure for Sinch SMS Webhooks
Let's structure our project with a
clientdirectory for the Vite frontend and aserverdirectory for the Node.js backend.Step 1: Create Project Directory and Frontend
Open your terminal and run the following commands:
Step 2: Create and Initialize Backend
Step 3: Project Structure
Your project structure should now look like this:
Step 4: Configure Environment Variables
Create a
.envfile inside theserverdirectory to store your Sinch credentials securely. Never commit this file to version control. Committing this file exposes your secret API keys and credentials_ creating a significant security risk.dotenvloads these intoprocess.envfor your Node.js application.PROJECT_ID,KEY_ID,KEY_SECRET: Log in to the Sinch Dashboard. Navigate to your Project, then find the API Keys or similar section. Create a new key if needed.SINCH_NUMBER: This is the number you've rented or configured within your Sinch account for sending SMS.SINCH_WEBHOOK_SECRET: You create this secret. Make it a strong, unpredictable string. You will enter this exact same string when configuring the webhook in the Sinch portal later.Step 5: Create Gitignore
In the root directory (
sinch-callback-app), create a.gitignorefile to prevent committing sensitive files and unnecessary directories:Initialize git and make your first commit:
Integrating the Sinch Node.js SDK for SMS Delivery Callbacks
Let's set up the Sinch SDK client in our backend.
Step 1: Create Sinch Client Configuration
Create a file
server/sinchClient.jsto initialize and export the Sinch client instance.dotenv. We export the initialized client for use in other parts of our application.How to Create a Secure Webhook Endpoint for Sinch Delivery Status Callbacks
This is the core of handling delivery statuses. We need an Express endpoint that Sinch can POST data to. This endpoint must handle raw request bodies for HMAC validation before attempting to parse JSON.
Step 1: Set up Basic Express Server
Modify
server/server.jswith the basic server structure.express.raw? Sinch's HMAC signature is calculated based on the raw, unparsed request body. If we letexpress.json()parse it first, the body gets modified, and the signature won't match. We applyexpress.raw({ type: 'application/json' })only to the webhook route.express.json()later? Other routes, like our upcoming/send-smsendpoint, will expect standard JSON payloads, so we apply theexpress.json()middleware after the specific webhook route.cors? The Vite development server runs on a different port than our backend server (e.g., 5173 vs. 3001). Browsers enforce the Same-Origin Policy, blocking requests between different origins unless the server explicitly allows it via CORS headers.cors()adds these headers. Security Note: The examplecors({ origin: '*' })allows requests from any origin. This is convenient for local development but highly insecure for production. In production, you must restrict the origin to your specific frontend domain(s), e.g.,cors({ origin: 'https://your-frontend-domain.com' }).Implementing HMAC Signature Validation for Sinch Webhook Security
This crucial step ensures that the callbacks received are genuinely from Sinch and haven't been tampered with. The code for this is included within the
/webhooks/sinch/deliveryroute handler inserver.js(Step 1 above).Critical Security Notes:
x-sinch-webhook-signature-algorithmheader (expected value:HmacSHA256).x-sinch-webhook-signature-timestampx-sinch-webhook-signature-noncex-sinch-webhook-signature-algorithmx-sinch-webhook-signaturecrypto.timingSafeEqual()for signature comparison. Regular string comparison (===) is vulnerable to timing attacks where attackers can infer signature validity by measuring response times.express.raw()middleware exclusively for webhook routes.Explanation of the HMAC Logic:
x-sinch-webhook-signature-timestamp,x-sinch-webhook-signature-nonce,x-sinch-webhook-signature-algorithm, andx-sinch-webhook-signaturefrom the request headers.express.raw).SINCH_WEBHOOK_SECRETyou defined in your.envfile.rawBody,nonce, andtimestampstrings, separated by a period (.).cryptomodule:sha256algorithm and yourwebhookSecret.signedData.base64format.crypto.timingSafeEqualto compare the calculated signature with the signature received in the header. This function prevents timing attacks. Important: Both buffers being compared must have the same byte length.401 Unauthorizedstatus.Processing and Handling Sinch SMS Delivery Status Updates
Once HMAC validation passes, you can safely parse and use the callback payload. This logic is also within the
/webhooks/sinch/deliveryroute handler (Step 1 in Section 3).Key Information in
message_delivery_report:message_id: The unique identifier for the message batch (usually matches thebatch_idreturned when sending). Use this to correlate the callback with the original message sent.status: The delivery status (e.g.,DELIVERED,FAILED,QUEUED_ON_CHANNEL,REJECTED,EXPIRED).channel_identity.identity: The recipient's phone number.reason: If the status isFAILED, this field often contains an error code or description.event_time: Timestamp of when the status event occurred.Sinch SMS Delivery Status Codes:
QUEUEDDISPATCHEDDELIVEREDFAILEDreasonfield, notify user, investigate carrier/number issuesEXPIREDREJECTEDUNKNOWNQUEUED_ON_CHANNELNote: Status codes may vary by Sinch API version and SMS channel. Consult Sinch Message Delivery States documentation for the complete list applicable to your implementation.
Action: In a real application, you would typically use the
message_idandstatusto update a record in your database associated with the sent message.Storing SMS Delivery Status in Your Database
For demonstration, we won't set up a full database. However, in a production scenario, you need persistent storage.
Conceptual Database Update:
Imagine you have a
messagestable with columns likemessage_id,recipient,body,sent_at,status,status_updated_at,failure_reason.Inside the callback handler (after validation and parsing):
Building a React Frontend with Vite for SMS Delivery Tracking
Let's create a simple React component to send an SMS via our backend.
Step 1: Modify
client/src/App.jsxReplace the contents of
client/src/App.jsxwith the following:Step 2: Basic Styling (Optional)
Add some basic styles to
client/src/App.css:Creating the SMS Sending API Endpoint with Sinch Node.js SDK
Now, let's implement the
/send-smsendpoint in our backend.Step 1: Update
server/server.jsReplace the placeholder
/send-smsroute with the actual implementation:sinchClientto call the Sinch SMS API, and returns the resultingbatch_id(message ID) or an error to the frontend.Best Practices for Error Handling in Sinch SMS Webhooks
try...catchblocks around Sinch API calls and callback processing.console.error./send-smsendpoint.fetchcall includes a.catch()block to handle network errors or exceptions.response.okto handle HTTP errors (like 4xx, 5xx) returned by the backend API.Production Considerations:
joiorexpress-validator).Testing Sinch SMS Delivery Callbacks Locally with ngrok
Testing webhooks locally requires exposing your backend server to the public internet so Sinch can reach it.
ngrokis perfect for this.Step 1: Configure Sinch Webhook
ngrokcomes in. Leave this blank for now.HTTP(orHTTPSif ngrok provides it).server/.envfile forSINCH_WEBHOOK_SECRET.MESSAGE_DELIVERY. This tells Sinch to send callbacks specifically for delivery status updates. You might find related triggers likeMESSAGE_INBOUNDorEVENT_DELIVERY- ensureMESSAGE_DELIVERY(or its equivalent for SMS status) is selected.Step 2: Run Backend and Frontend
Run Backend Server:
You should see
Server listening on port 3001.Run Frontend Dev Server: Open another terminal window.
Your React app should open in your browser (usually at
http://localhost:5173).Step 3: Use ngrok
ForwardingURLs. Copy thehttpsURL (e.g.,https://random-string.ngrok-free.app).Step 4: Update Sinch Webhook URL
httpsngrok URL into the Target URL field, appending your specific webhook path:https://random-string.ngrok-free.app/webhooks/sinch/deliveryStep 5: Send an SMS and Watch for Callbacks
http://localhost:5173)./send-smsrequest and the response from the Sinch API./webhooks/sinch/deliveryendpoint:message_id,status(e.g.,DELIVERED), etc./webhooks/sinch/deliverypath on the ngrok tunnel.Testing HMAC Failure (Optional):
SINCH_WEBHOOK_SECRETin your.envfile to something incorrect.node server.js).Using
curlto Simulate Callbacks:You can manually test the endpoint, but you need a valid payload and correctly calculated HMAC headers.
x-sinch-webhook-signaturebased on the captured payload, a nonce, a timestamp, and your secret.curlRequest:Troubleshooting Sinch Webhook and Delivery Status Issues
HMAC Validation Failures
Symptom: Backend logs show "HMAC validation failed: Signatures do not match"
Common Causes:
SINCH_WEBHOOK_SECRETin your.envfile doesn't match the secret configured in Sinch Dashboard. These must be identical (case-sensitive).express.json()parses the body before HMAC validation. Ensureexpress.raw()is applied to the webhook route before any JSON parsing middleware.req.body.toString('utf8')for consistency.crypto.timingSafeEqual()must have identical length.Solution: Check webhook secret, verify middleware order, log both signatures for comparison, ensure base64 encoding.
Webhook Not Receiving Callbacks
Symptom: SMS sends successfully but no callback arrives at webhook endpoint
Common Causes:
https://xxx.ngrok-free.app/webhooks/sinch/deliveryMESSAGE_DELIVERYtrigger is enabled and associated with your Service Plan ID.Solution: Verify webhook URL in Sinch Dashboard, restart ngrok and update URL, check server is running on correct port, enable webhook triggers.
ngrok Connection Issues
Symptom: ngrok shows errors or "Failed to start tunnel"
Common Causes:
PORTin.envor kill the conflicting process.ngrok authtoken YOUR_TOKENwith token from ngrok dashboard.Solution: Kill processes on port 3001 (
lsof -ti:3001 | xargs kill), authenticate ngrok, check ngrok dashboard for account limits.CORS Errors in Frontend
Symptom: Browser console shows "CORS policy: No 'Access-Control-Allow-Origin' header"
Common Causes:
cors()middleware not applied to Express app.Solution: Apply
cors()middleware early inserver.js, usecors({ origin: '*' })for development only, specify exact frontend origin in production:cors({ origin: 'https://your-frontend.com' }).SMS Not Sending
Symptom:
/send-smsendpoint returns errorsCommon Causes:
SINCH_PROJECT_ID,SINCH_KEY_ID, orSINCH_KEY_SECRETincorrect or expired.+12025551234). Missing+or country code causes rejection.SINCH_NUMBERnot provisioned, or alphanumeric sender ID not allowed in recipient country.Solution: Verify credentials in Sinch Dashboard, format numbers as E.164, provision sender ID, upgrade trial account, check SDK documentation for current API.
Sinch SMS Delivery Callback FAQs
How do I test webhooks without ngrok?
Deploy your backend to a hosting service with a public URL (Vercel, Heroku, Railway, AWS). Use the deployed URL in Sinch Dashboard. For development, ngrok is simplest, but alternatives include localtunnel, serveo, or Cloudflare Tunnel.
What happens if my webhook endpoint is down?
Sinch retries failed webhook deliveries multiple times over several hours. Implement retry logic and ensure your endpoint responds with HTTP 200-299 within 10 seconds. Prolonged failures may cause Sinch to disable the webhook.
How do I handle multiple concurrent callbacks?
Use async/await properly and avoid blocking operations. For high volume, implement a message queue (RabbitMQ, AWS SQS, Redis Bull) to process callbacks asynchronously. Store callbacks in a database immediately, then process in background workers.
Can I use Sinch webhooks with serverless functions?
Yes, but ensure your function:
express.raw()or equivalent to preserve raw bodyServerless platforms: AWS Lambda (with API Gateway), Vercel Functions, Netlify Functions, Google Cloud Functions.
How do I verify webhook signature without Express?
Extract raw body as Buffer, get headers, calculate HMAC-SHA256:
What should I store in my database?
Minimum recommended fields:
message_id(from Sinch batch ID)recipient(phone number)message_body(SMS content)status(current delivery status)status_updated_at(timestamp)failure_reason(if failed)sent_at(original send time)callback_count(number of callbacks received for idempotency)How do I implement replay attack prevention?
What are Sinch's webhook retry policies?
Sinch retries failed webhooks (non-2xx responses or timeouts) with exponential backoff. Typical retry schedule:
Exact timing depends on failure type. Implement idempotency to handle duplicate callbacks safely. Check
callback_countin your database before processing.