code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / Sinch

How to Build Sinch WhatsApp Integration with Node.js & Express (2025 Guide)

Learn how to integrate WhatsApp Business API with Node.js and Express using Sinch Conversation API. Complete tutorial with code examples for sending messages, webhooks, authentication, and deployment.

Production-Ready WhatsApp Integration with Node.js, Express, and Sinch Conversation API

Learn how to integrate WhatsApp Business API into your Node.js application using Express and the Sinch Conversation API. This comprehensive tutorial covers authentication, sending messages, webhook setup, security best practices, and production deployment.

Note: This guide uses the current Sinch Conversation API. Older standalone WhatsApp APIs are deprecated. Always reference the Conversation API documentation for current methods.

Project Overview and Goals

What you'll build:

A Node.js Express application that:

  1. Sends WhatsApp messages: Initiate conversations using approved templates or send free-form messages within the 24-hour customer service window.
  2. Receives WhatsApp messages: Process incoming messages and delivery receipts through Sinch webhooks.
  3. Provides a REST API: Expose endpoints to trigger outgoing messages.

Problem solved:

Connect your backend systems to WhatsApp for customer communication, support, notifications, and engagement. Use cases include order confirmations, customer support tickets, appointment reminders, and marketing campaigns reaching 2+ billion WhatsApp users worldwide.

Technologies Used:

  • Node.js: JavaScript runtime for scalable server-side applications.
  • Express: Minimal Node.js web framework for building APIs.
  • Sinch Conversation API: Unified platform for managing conversations across WhatsApp, SMS, RCS, and other channels.
  • dotenv: Load environment variables from .env files.
  • axios: Promise-based HTTP client for API requests.
  • Express Raw & JSON Parsers: Parse request bodies. express.raw() must run before express.json() for webhook signature verification.
  • crypto: Node.js built-in module for cryptographic operations.
  • (Optional) Prisma: Database toolkit for storing conversation data.
  • (Optional) ngrok: Expose local servers for webhook testing.

Prerequisites:

  • Node.js 18+ and npm: Download Node.js
  • Sinch Account: Register at sinch.com with postpay billing enabled. WhatsApp channel access requires postpay billing. Contact Sinch support to enable this on your account.
  • Sinch Conversation API Access: Verify the Conversation API is enabled in your account settings.
  • Sinch Project and App: Create a project in the Sinch Customer Dashboard. Navigate to Apps > Create App > Select "Conversation API" as the app type. Note your Project ID.
  • Sinch Access Keys: In your app settings, go to Access Keys > Generate New Key. Copy both Access Key ID and Access Secret immediately – the secret appears only once.
  • WhatsApp Sender ID: Provision a WhatsApp Business number through Sinch Customer Dashboard > Numbers > Buy Number or provision existing. Approval typically takes 24–48 hours. The Sender ID (also called app_id or claimed_identity) appears in your app's channel configuration.
  • ngrok: For local webhook testing. Download ngrok
  • Basic knowledge of Node.js, Express, REST APIs, and async JavaScript.

1. Setting Up Your Node.js WhatsApp Project

Initialize your Node.js project and install dependencies for WhatsApp messaging.

1.1. Create Project Directory:

bash
mkdir sinch-whatsapp-integration
cd sinch-whatsapp-integration

1.2. Initialize npm Project:

bash
npm init -y

1.3. Install Dependencies:

bash
npm install express dotenv axios crypto
# Or using yarn:
# yarn add express dotenv axios crypto
  • express: Web framework
  • dotenv: Load environment variables from .env
  • axios: HTTP client for Sinch API calls
  • crypto: Built-in module for webhook signature verification
  • Note: Use Express's built-in express.json() and express.raw() middleware. express.raw() must run before express.json() for webhook routes to access req.rawBody for signature verification.

1.4. Project Structure:

Create the following directory structure:

text
sinch-whatsapp-integration/
├── src/
│   ├── controllers/
│   │   ├── messageController.js
│   │   └── webhookController.js
│   ├── routes/
│   │   ├── messageRoutes.js
│   │   └── webhookRoutes.js
│   ├── services/
│   │   └── sinchService.js
│   ├── middleware/
│   │   └── verifySinchWebhook.js
│   └── app.js              # Express app setup
├── .env                    # Environment variables (DO NOT COMMIT)
├── .gitignore              # Git ignore file
└── package.json

1.5. Create .gitignore:

Create a .gitignore file to prevent committing sensitive files:

text
# .gitignore
node_modules/
.env
*.log
prisma/migrations/*.sql # If using Prisma

1.6. Create .env File:

Create a .env file in the root directory:

dotenv
# .env - Fill these values from Sinch dashboard
PORT=3000
NODE_ENV=development # Set to 'production' in deployment

# Sinch Credentials
SINCH_PROJECT_ID=YOUR_SINCH_PROJECT_ID
SINCH_ACCESS_KEY_ID=YOUR_SINCH_ACCESS_KEY_ID
SINCH_ACCESS_SECRET=YOUR_SINCH_ACCESS_SECRET
WHATSAPP_SENDER_ID=YOUR_WHATSAPP_SENDER_ID_FROM_SINCH # The ID for your WhatsApp number

# Webhook Secret (generate using: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
SINCH_WEBHOOK_SECRET=YOUR_STRONG_RANDOM_SECRET_FOR_WEBHOOK_VERIFICATION

# Sinch API Region (us or eu - check your app config in dashboard)
SINCH_REGION=us # or eu

# Optional: Database URL (if using Prisma)
# DATABASE_URL="postgresql://user:password@host:port/database?schema=public"

# Optional: Logging Level
# LOG_LEVEL=info

Key decisions:

  • dotenv keeps credentials out of source code
  • Structured directories (controllers, routes, services) improve maintainability
  • axios provides simple HTTP client functionality
  • crypto secures webhook endpoints

2. Implementing WhatsApp Message Sending with Sinch

Implement the core logic for sending and receiving WhatsApp messages through the Sinch API.

2.1. Sending WhatsApp Messages via Sinch (src/services/sinchService.js)

This service handles all Sinch Conversation API interactions.

javascript
// src/services/sinchService.js
const axios = require('axios');
const crypto = require('crypto');
require('dotenv').config();

const {
    SINCH_PROJECT_ID,
    SINCH_ACCESS_KEY_ID,
    SINCH_ACCESS_SECRET,
    WHATSAPP_SENDER_ID,
    SINCH_REGION // 'us' or 'eu'
} = process.env;

// Input validation
if (!SINCH_PROJECT_ID || !SINCH_ACCESS_KEY_ID || !SINCH_ACCESS_SECRET || !WHATSAPP_SENDER_ID || !SINCH_REGION) {
    console.error("FATAL ERROR: Missing required Sinch environment variables.");
    process.exit(1);
}

const CONVERSATION_API_URL_BASE = `https://${SINCH_REGION}.conversation.api.sinch.com/v1/projects/${SINCH_PROJECT_ID}`;

// --- Authentication Helper ---
/**
 * Generates the Sinch Authentication Header value.
 * IMPORTANT: Verify stringToSign format against current Sinch documentation:
 * https://developers.sinch.com/docs/conversation/api-reference/#authentication/application-signed-requests
 *
 * @param {string} method - HTTP Method (POST, GET, etc.)
 * @param {string} path - Request path (e.g., /messages:send)
 * @param {string} [requestBody=''] - Stringified JSON request body or empty string for GET
 * @returns {string} - Authorization header value
 */
const generateSinchAuthHeader = (method, path, requestBody = '') => {
    const httpVerb = method.toUpperCase();
    const contentMd5 = crypto.createHash('md5').update(requestBody, 'utf-8').digest('base64');
    const contentType = requestBody ? 'application/json; charset=utf-8' : '';
    const timestamp = new Date().toISOString();
    const canonicalizedHeaders = `x-timestamp:${timestamp}`;
    const canonicalizedResource = path;

    const stringToSign = `${httpVerb}\n${contentMd5}\n${contentType}\n${canonicalizedHeaders}\n${canonicalizedResource}`;

    // IMPORTANT: SINCH_ACCESS_SECRET from dashboard is Base64-encoded
    const signature = crypto.createHmac('sha256', Buffer.from(SINCH_ACCESS_SECRET, 'base64'))
        .update(stringToSign, 'utf-8')
        .digest('base64');

    return `Application ${SINCH_ACCESS_KEY_ID}:${signature}`;
};

// --- Sending Logic ---

/**
 * Send a message via Sinch Conversation API
 * @param {string} recipientPhoneNumber - E.164 formatted phone number
 * @param {object} messagePayload - Sinch message object (e.g., { text_message: { text: 'Hello!' } })
 * @returns {Promise<object>} - Response data from Sinch API
 * @throws {Error} - If API call fails
 */
const sendMessage = async (recipientPhoneNumber, messagePayload) => {
    const path = '/messages:send';
    const url = `${CONVERSATION_API_URL_BASE}${path}`;
    const method = 'POST';

    // Note: Using contact_id for WhatsApp is standard.
    // Alternative: identified_by: { channel: 'WHATSAPP', identity: recipientPhoneNumber }
    const body = {
        app_id: WHATSAPP_SENDER_ID,
        recipient: {
            contact_id: recipientPhoneNumber
        },
        message: messagePayload,
        channel_priority_order: ["WHATSAPP"]
    };

    const requestBodyString = JSON.stringify(body);
    const authorization = generateSinchAuthHeader(method, path, requestBodyString);
    const timestamp = new Date().toISOString();

    try {
        console.log(`Sending message to ${recipientPhoneNumber} via Sinch…`);
        const response = await axios({
            method: method,
            url: url,
            data: requestBodyString,
            headers: {
                'Content-Type': 'application/json; charset=utf-8',
                'x-timestamp': timestamp,
                'Authorization': authorization
            }
        });
        console.log('Sinch API Response:', response.data);
        return response.data;
    } catch (error) {
        console.error('Error sending message via Sinch:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
        if (error.response) {
             error.statusCode = error.response.status;
        }
        throw error;
    }
};

/**
 * Send a simple text message
 * @param {string} recipientPhoneNumber
 * @param {string} text
 */
const sendTextMessage = async (recipientPhoneNumber, text) => {
    const messagePayload = {
        text_message: { text: text }
    };
    return sendMessage(recipientPhoneNumber, messagePayload);
};

/**
 * Send a pre-approved template message
 * Required when initiating conversations or outside the 24-hour window
 * @param {string} recipientPhoneNumber
 * @param {string} templateId - Approved template ID
 * @param {string[]} [params] - Optional parameters for template placeholders
 */
const sendTemplateMessage = async (recipientPhoneNumber, templateId, params = []) => {
    // Verify parameter names in your Sinch dashboard template definition
    const parameters = params.reduce((acc, value, index) => {
        acc[`param${index + 1}`] = value;
        return acc;
    }, {});

    const messagePayload = {
        template_message: {
            omni_template: {
                template_id: templateId,
                version: "latest",
                language_code: "en_US",
                parameters: parameters
            }
        }
    };
    return sendMessage(recipientPhoneNumber, messagePayload);
};

module.exports = {
    sendMessage,
    sendTextMessage,
    sendTemplateMessage,
    generateSinchAuthHeader
};

Key implementation details:

  • Rate Limits: Sinch enforces rate limits based on account tier. Implement exponential backoff with 3 retries for 429 responses. Default limits: 10 requests/second (standard), 50 requests/second (enterprise).
  • Error Handling: Attach statusCode to thrown errors for upstream handling. Log full error details but avoid exposing internal errors to clients.
  • Authentication: Verify stringToSign format matches current Sinch documentation. Access Secret from dashboard is Base64-encoded.
  • Recipient Identification: Use contact_id: phoneNumber for WhatsApp. Alternative: identified_by: { channel: 'WHATSAPP', identity: phoneNumber }.
  • Region Selection: Set SINCH_REGION to us or eu based on your app configuration in the Sinch dashboard.

3. Setting Up WhatsApp Webhooks for Incoming Messages

Create an endpoint to receive Sinch callbacks for incoming WhatsApp messages and message events.

3.1. Webhook Verification Middleware (src/middleware/verifySinchWebhook.js)

Verify incoming webhook requests originate from Sinch using signature validation.

javascript
// src/middleware/verifySinchWebhook.js
const crypto = require('crypto');
require('dotenv').config();

const { SINCH_WEBHOOK_SECRET } = process.env;

/**
 * Express middleware to verify Sinch webhook signatures
 * IMPORTANT: Verify stringToSign format against Sinch documentation:
 * https://developers.sinch.com/docs/conversation/webhook-verification/
 */
const verifySinchWebhook = (req, res, next) => {
    if (!SINCH_WEBHOOK_SECRET) {
        console.error('CRITICAL: SINCH_WEBHOOK_SECRET not set. Webhook verification cannot proceed.');
        return res.status(500).send('Webhook secret not configured');
    }

    const signatureHeader = req.headers['x-sinch-signature'];
    const timestampHeader = req.headers['x-sinch-timestamp'];

    if (!signatureHeader || !timestampHeader) {
        console.error('Webhook Error: Missing signature headers (x-sinch-signature or x-sinch-timestamp)');
        return res.status(400).send('Missing signature headers');
    }

    // Validate timestamp (within 5 minutes)
    const requestTimestamp = parseInt(timestampHeader, 10);
    const currentTimestamp = Math.floor(Date.now() / 1000);
    const timestampTolerance = 300; // 5 minutes in seconds

    if (isNaN(requestTimestamp) || Math.abs(currentTimestamp - requestTimestamp) > timestampTolerance) {
        console.error(`Webhook Error: Timestamp validation failed. Request: ${requestTimestamp}, Current: ${currentTimestamp}`);
        return res.status(400).send('Timestamp validation failed');
    }

    // CRITICAL: req.rawBody required. Ensure express.raw() runs BEFORE express.json()
    if (!req.rawBody || req.rawBody.length === 0) {
         console.error('Webhook Error: Missing req.rawBody. Configure raw body parser BEFORE JSON parser.');
         return res.status(500).send('Internal server error: Raw body not available');
    }

    const stringToSign = `${req.rawBody}|${timestampHeader}`;

    try {
        const expectedSignature = crypto.createHmac('sha256', SINCH_WEBHOOK_SECRET)
                                       .update(stringToSign)
                                       .digest('base64');

        const signatures = signatureHeader.split(',');
        const providedSignature = signatures[0].trim();

        if (signatures.length > 1) {
            console.warn(`Multiple signatures received. Verifying first: ${providedSignature}`);
        }

        const signaturesMatch = crypto.timingSafeEqual(
            Buffer.from(providedSignature),
            Buffer.from(expectedSignature)
        );

        if (!signaturesMatch) {
            console.error('Webhook Error: Invalid signature');
            return res.status(403).send('Invalid signature');
        }

        console.log('Sinch webhook signature verified successfully');
        next();

    } catch (error) {
        console.error('Webhook Error: Exception during signature verification:', error);
        return res.status(500).send('Internal server error during signature verification');
    }
};

module.exports = verifySinchWebhook;

Security measures:

  • Timestamp Validation: Reject requests with timestamps older than 5 minutes to prevent replay attacks.
  • Raw Body Requirement: Signature verification requires the unmodified request body. Configure express.raw() before express.json().
  • Timing-Safe Comparison: Use crypto.timingSafeEqual() to prevent timing attacks.
  • Multiple Signatures: If multiple signatures appear (rare, from proxies), verify the first one.

3.2. Webhook Controller (src/controllers/webhookController.js)

Handle logic after webhook signature verification.

javascript
// src/controllers/webhookController.js
const sinchService = require('../services/sinchService');

const handleIncomingEvent = async (req, res) => {
    const event = req.body;

    console.log(`Received Verified Sinch Webhook Event: ${event.event_id || 'Unknown ID'}`);

    try {
        if (event.message_inbound) {
            await handleInboundMessage(event.message_inbound);
        } else if (event.message_delivery) {
            await handleDeliveryReceipt(event.message_delivery);
        } else if (event.contact_create) {
            console.log(`Contact created: ${event.contact_create.contact_id}`);
        } else if (event.contact_merge) {
            console.log(`Contacts merged: ${event.contact_merge.merged_contact_ids} into ${event.contact_merge.contact_id}`);
        } else if (event.conversation_start) {
             console.log(`Conversation started: ${event.conversation_start.conversation_id}`);
        }

        res.status(200).send('Webhook received and processed successfully');

    } catch (error) {
        console.error(`Error processing webhook event ${event.event_id || 'Unknown ID'}:`, error);
        res.status(500).send('Error processing webhook event');
    }
};

// --- Specific Event Handlers ---

const handleInboundMessage = async (inboundEvent) => {
    console.log('Handling Inbound Message:', inboundEvent.message_id);

    const contactId = inboundEvent.contact_message?.contact_id;
    const appId = inboundEvent.app_id;
    const message = inboundEvent.contact_message?.message;
    const messageId = inboundEvent.message_id;

    if (!contactId || !message || !messageId) {
        console.error("Inbound message missing critical fields. Skipping.", inboundEvent);
        return;
    }

    // Example: Echo Bot with 24-hour window awareness
    if (message.text_message) {
        const receivedText = message.text_message.text;
        console.log(`Received text from ${contactId}: "${receivedText}"`);

        // IMPORTANT: Production apps MUST track last interaction time in database
        const isWithinWindow = true; // Replace with: checkDbForRecentInteraction(contactId)

        if (isWithinWindow) {
            if (receivedText.trim().toUpperCase() === 'STOP') {
                console.log(`Opt-out request from ${contactId}`);
                // Mark user as opted-out in database
                await sinchService.sendTextMessage(contactId, "You have been opted out of messages.");
            } else {
                try {
                    const replyText = `You said: "${receivedText}"`;
                    await sinchService.sendTextMessage(contactId, replyText);
                    console.log(`Sent reply to ${contactId}`);
                } catch (error) {
                    console.error(`Failed to send reply to ${contactId}:`, error);
                }
            }
        } else {
            console.log(`Message from ${contactId} outside 24-hour window. Cannot send free-form reply.`);
            // Send template message if appropriate
        }

    } else if (message.media_message) {
        console.log(`Received media from ${contactId}: ${message.media_message.url}`);
        // Handle media (download, process, store URL)

    } else if (message.choice_response_message) {
         console.log(`Received button/list reply from ${contactId}: ${message.choice_response_message.choice_id}`);
         // Handle interactive message replies

    } else {
        const messageType = Object.keys(message)[0];
        console.log(`Received ${messageType} from ${contactId}`);
    }
};

const handleDeliveryReceipt = async (deliveryEvent) => {
    console.log('Handling Delivery Receipt:', deliveryEvent.message_id);

    const messageId = deliveryEvent.message_id;
    const contactId = deliveryEvent.contact_id;
    const status = deliveryEvent.status; // DELIVERED, FAILED, READ, PENDING
    const timestamp = deliveryEvent.processed_time;
    const reason = deliveryEvent.reason;

    console.log(`Message ${messageId} to ${contactId}: ${status} at ${timestamp}`);

    // Update message status in database

    if (status === 'FAILED') {
        console.error(`Message ${messageId} FAILED: ${reason?.code || 'N/A'} - ${reason?.description || 'No description'}`);
        // Trigger alerts for failed messages
    } else if (status === 'READ') {
         console.log(`Message ${messageId} was READ at ${timestamp}`);
         // Trigger analytics or follow-up logic
    }
};

module.exports = {
    handleIncomingEvent
};

Implementation notes:

  • 24-Hour Window Tracking: Production apps must store last interaction timestamp per contact in a database. Query this before sending free-form replies. Outside the 24-hour window, only template messages work.
  • Opt-Out Handling: Check for "STOP" keywords and update your database to respect user preferences. Send confirmation when users opt out.
  • Message Types: Handle text_message, media_message (images, videos, documents), and choice_response_message (interactive button/list replies).
  • Delivery Receipts: Track message status (PENDING → DELIVERED → READ or FAILED). Log failures with reason codes for troubleshooting.

3.3. Webhook Route (src/routes/webhookRoutes.js)

Define the /webhook endpoint and apply verification middleware.

javascript
// src/routes/webhookRoutes.js
const express = require('express');
const webhookController = require('../controllers/webhookController');
const verifySinchWebhook = require('../middleware/verifySinchWebhook');

const router = express.Router();

// IMPORTANT: Configure express.raw() in app.js BEFORE express.json() for this route

router.post(
    '/',
    verifySinchWebhook, // Verifies using req.rawBody
    express.json(), // Parses verified body into req.body
    webhookController.handleIncomingEvent
);

module.exports = router;

Critical middleware order:

  1. express.raw() – captures raw body for signature verification
  2. verifySinchWebhook – validates signature using raw body
  3. express.json() – parses verified body for controller

Incorrect order breaks signature validation.

4. Building a REST API for Message Sending

Create an API endpoint to trigger WhatsApp message sending from your application.

4.1. Message Controller (src/controllers/messageController.js)

javascript
// src/controllers/messageController.js
const sinchService = require('../services/sinchService');

const sendMessageApi = async (req, res, next) => {
    const { to, type, text, templateId, params } = req.body;

    if (!to || !type) {
        return res.status(400).json({ error: 'Missing required fields: "to" and "type"' });
    }

    // Validate E.164 phone number format
    if (!/^\+?[1-9]\d{1,14}$/.test(to)) {
         return res.status(400).json({ error: 'Invalid phone number format. Use E.164 (e.g., +14155552671)' });
    }

    try {
        let result;

        if (type === 'text') {
            if (!text) {
                return res.status(400).json({ error: 'Missing "text" field for type "text"' });
            }
            console.log(`API request to send text to ${to}`);
            result = await sinchService.sendTextMessage(to, text);

        } else if (type === 'template') {
            if (!templateId) {
                return res.status(400).json({ error: 'Missing "templateId" field for type "template"' });
            }
            if (params && !Array.isArray(params)) {
                 return res.status(400).json({ error: '"params" must be an array of strings' });
            }
            const templateParams = params || [];
            console.log(`API request to send template ${templateId} to ${to}`);
            result = await sinchService.sendTemplateMessage(to, templateId, templateParams);

        } else {
            return res.status(400).json({ error: 'Invalid message type. Use "text" or "template"' });
        }

        // Store sent message in database

        return res.status(200).json({
            success: true,
            message: 'Message sent successfully',
            messageId: result.message_id,
            conversationId: result.conversation_id
        });

    } catch (error) {
        console.error('Error in sendMessageApi:', error);
        const statusCode = error.statusCode || 500;
        return res.status(statusCode).json({
            success: false,
            error: error.message || 'Failed to send message'
        });
    }
};

module.exports = {
    sendMessageApi
};

Validation and error handling:

  • Validate E.164 phone number format: +[country_code][number] (e.g., +14155552671)
  • Return specific error messages with appropriate HTTP status codes (400 for validation, 500 for server errors)
  • Propagate status codes from Sinch API failures
  • Store sent messages in database for audit trails and conversation tracking

Frequently Asked Questions About WhatsApp Integration with Node.js

How do I integrate WhatsApp messaging with Node.js using Sinch?

Install required packages (express, axios, dotenv, crypto), configure Sinch credentials in .env, implement HMAC-SHA256 authentication, create message-sending endpoints, and set up webhook handlers. The Conversation API provides a unified interface for WhatsApp Business messaging.

What is the Sinch Conversation API and how does it differ from standalone WhatsApp APIs?

The Sinch Conversation API manages conversations across WhatsApp, SMS, RCS, and other channels through a single API. It provides centralized authentication, consistent message formatting, and multi-channel support. Standalone WhatsApp APIs are deprecated – use the Conversation API for all new integrations.

How do I authenticate API requests to Sinch Conversation API?

Create a string-to-sign containing: HTTP method, MD5 hash of request body, content type, x-timestamp header, and resource path. Sign with your Base64-decoded Access Secret using HMAC-SHA256. Format the header as: Application {ACCESS_KEY_ID}:{signature}. Include x-timestamp and Authorization headers in every request.

What is the WhatsApp 24-hour customer service window?

After a user messages you, you have 24 hours to send free-form replies. Outside this window, only pre-approved template messages work. Store the last interaction timestamp per contact in your database to check window status before sending messages.

How do I verify Sinch webhook signatures for security?

Concatenate raw request body + | + timestamp value. Generate HMAC-SHA256 signature with your webhook secret, encode as Base64, and compare with x-sinch-signature using crypto.timingSafeEqual(). Validate timestamp is within 5 minutes to prevent replay attacks.

What are the required prerequisites for Sinch WhatsApp integration?

Sinch account with postpay billing, Conversation API access, project and app in Sinch dashboard, Access Key ID and Secret, provisioned WhatsApp Business number (Sender ID), Node.js 18+, and ngrok for local testing. Store credentials in environment variables.

How do I send WhatsApp template messages with Sinch?

Use template_message with omni_template structure. Specify template_id (from approved templates), version ("latest"), language_code ("en_US"), and parameters with placeholder values. Required for initiating conversations or messaging outside the 24-hour window. Verify parameter names in your Sinch dashboard template definition.

What is the correct middleware order for Sinch webhooks in Express?

Order: express.raw() → signature verification middleware → express.json(). Parsing before verification breaks HMAC validation, which requires the unmodified raw body.

How do I handle incoming WhatsApp messages with Sinch?

Extract contact_id (sender's phone), message object (text, media, or interactive responses), and message_id from message_inbound events. Check 24-hour window status in your database. Send replies with sendTextMessage() within the window or templates outside it. Implement keyword handling and opt-out processing.

What are common Sinch WhatsApp integration errors and solutions?

Authentication failures: Verify Access Secret is Base64-decoded and signature calculation matches documentation. Webhook signature mismatches: Ensure raw body parser runs before JSON parser. Message failures: Check 24-hour window status, verify template IDs, validate E.164 format, confirm Sender ID provisioning. 403 errors: Enable postpay billing in your Sinch account.


Next Steps for Production WhatsApp Integration

Your WhatsApp messaging integration handles secure webhook verification, sends both free-form and template messages, processes incoming messages and delivery receipts, and exposes a REST API for message triggering.

Production enhancements:

  1. Message Queueing: Implement Redis or RabbitMQ for high-volume message processing
  2. Database Integration: Add Prisma for conversation storage and 24-hour window tracking
  3. Monitoring: Integrate Sentry or Datadog for error tracking and performance monitoring
  4. Retry Logic: Implement exponential backoff (100ms, 200ms, 400ms) for transient failures
  5. HTTPS Deployment: Deploy with SSL certificates for production webhook security
  6. Rate Limiting: Add express-rate-limit to prevent API abuse (100 requests/15 minutes per IP)

Additional resources:

Your integration follows WhatsApp Business API best practices and is ready to power customer communications at scale.