Integrating Sinch WhatsApp with Next.js: A Developer's Guide
Build robust WhatsApp messaging capabilities into your Next.js application using the Sinch Conversation API. This guide provides a step-by-step walkthrough for sending and receiving WhatsApp messages, handling configuration, managing security, and deploying a production-ready solution.
We will create a Next.js application with API routes to interact with the Sinch Conversation API. This enables sending outbound WhatsApp messages and processing inbound messages received via Sinch webhooks. By the end, you'll have a functional integration capable of basic two-way WhatsApp communication, deployed and ready for testing.
Project Overview and Goals
Objective: Implement a system within a Next.js application to send outbound WhatsApp messages and receive inbound messages via the Sinch Conversation API.
Problem Solved: This integration enables businesses to programmatically communicate with users on WhatsApp for notifications, customer support, alerts, and other use cases directly from their web application backend.
Technologies:
- Next.js: A React framework for building full-stack web applications. Chosen for its hybrid static & server rendering, API routes, and excellent developer experience.
- Sinch Conversation API: A unified API for managing conversations across multiple channels, including WhatsApp. Chosen for its robust features and specific WhatsApp capabilities.
- Node.js: The JavaScript runtime environment Next.js is built upon.
Architecture:
The system involves the end user's WhatsApp client communicating with the WhatsApp platform, which relays messages to and from the Sinch Conversation API. Your Next.js application interacts with the Sinch API to send messages (via API calls) and receive messages/events (via webhooks). Application logic triggers outbound messages, and optionally, data can be logged or stored in a database.
Outcome: A Next.js application with:
- An API endpoint (
/api/send-whatsapp
) to trigger sending WhatsApp messages via Sinch. - An API endpoint (
/api/sinch-webhook
) to receive incoming message notifications from Sinch. - Secure handling of credentials and robust error handling.
- Instructions for deployment and testing.
Prerequisites:
- Node.js (LTS version recommended) and npm/yarn installed.
- A Sinch account. For sending messages beyond initial free credits or for production volumes, postpay billing will likely need to be enabled. A trial account may suffice for initial basic testing within its limits. Contact Sinch support or your account manager for details on billing and enabling postpay if required.
- Access granted to the Sinch Conversation API within your Sinch account.
- A configured Sinch Conversation API App (we'll cover parts of this setup).
- A provisioned WhatsApp Sender ID via Sinch (using the Embedded Signup process in the Sinch dashboard).
- Familiarity with JavaScript, React, and basic API concepts.
1. Setting up the Project
Let's initialize a new Next.js project and configure the necessary environment variables.
1.1 Create Next.js App
Open your terminal and run:
npx create-next-app@latest sinch-whatsapp-nextjs
# Follow the prompts (e.g., select TypeScript: No, ESLint: Yes, Tailwind CSS: No, `src/` directory: No, App Router: Yes (Recommended), Import alias: No)
cd sinch-whatsapp-nextjs
1.2 Environment Variables
Sinch requires several credentials. We will store these securely in environment variables. Create a file named .env.local
in the root of your project.
Never commit .env.local
to version control. Add it to your .gitignore
file if it's not already there.
Important: Replace all YOUR_...
placeholders below with your actual credentials obtained from the Sinch dashboard (see Section 4).
# .env.local
# Sinch Project & API Access Credentials
# Found in Sinch Dashboard -> Your Project -> API Keys
SINCH_PROJECT_ID=YOUR_SINCH_PROJECT_ID
SINCH_KEY_ID=YOUR_SINCH_ACCESS_KEY_ID
SINCH_KEY_SECRET=YOUR_SINCH_ACCESS_KEY_SECRET
# Sinch Conversation API App Credentials
# Found in Sinch Dashboard -> Conversation API -> Your App
SINCH_APP_ID=YOUR_CONVERSATION_APP_ID
# Typically 'us', 'eu', or 'br' - Match your Conversation API App region
SINCH_REGION=us
# Sinch WhatsApp Channel Credentials (Specific to your configured WhatsApp Sender)
# Found during WhatsApp Sender setup or in App Configuration
SINCH_WHATSAPP_SENDER_ID=YOUR_WHATSAPP_SENDER_ID # Often the phone number in E.164 format
# Note: This token is essential for configuring the WhatsApp channel in the Sinch App (dashboard setup).
# The API calls in this guide use Basic Authentication derived from SINCH_KEY_ID/SECRET for sending.
SINCH_WHATSAPP_BEARER_TOKEN=YOUR_WHATSAPP_SENDER_BEARER_TOKEN # Provided during sender setup
# Application URL (Needed for Webhook setup in Sinch Dashboard)
# Use your deployed URL in production. For local dev, use an ngrok URL.
NEXT_PUBLIC_APP_URL=http://localhost:3000 # Replace with deployment or ngrok URL later
- Purpose: Storing sensitive credentials outside your codebase enhances security. Next.js automatically loads variables from
.env.local
intoprocess.env
. - Obtaining Values: See Section 4 for detailed steps on finding these values in the Sinch Customer Dashboard.
NEXT_PUBLIC_APP_URL
: This needs to be the publicly accessible base URL of your application, as Sinch needs it to send webhooks. It's prefixed withNEXT_PUBLIC_
for convention, though primarily used for external configuration here.
1.3 Project Structure (App Router Example)
We'll primarily work within the app/api/
directory for our backend logic.
sinch-whatsapp-nextjs/
├── app/
│ ├── api/
│ │ ├── send-whatsapp/
│ │ │ └── route.js # Endpoint to send messages
│ │ └── sinch-webhook/
│ │ └── route.js # Endpoint to receive webhooks
│ ├── layout.js
│ └── page.js # Optional simple UI for testing
├── .env.local # Your secret credentials (DO NOT COMMIT)
├── .env.local.example # Example structure for others (Commit this)
├── .gitignore
├── next.config.js
├── package.json
└── README.md
- Architectural Decision: Using Next.js API routes keeps the backend logic colocated with the frontend (if any), simplifying development and deployment for full-stack applications. The App Router provides a modern, streamlined approach.
2. Implementing Core Functionality: Sending Messages
Let's create the API endpoint responsible for sending an outbound WhatsApp message via Sinch.
File: app/api/send-whatsapp/route.js
// app/api/send-whatsapp/route.js
import { NextResponse } from 'next/server';
// Basic Authentication Token Generation (Should be cached in production)
function getSinchAuthToken() {
const keyId = process.env.SINCH_KEY_ID;
const keySecret = process.env.SINCH_KEY_SECRET;
if (!keyId || !keySecret) {
throw new Error(""Sinch Key ID or Secret not configured in environment variables."");
}
const credentials = `${keyId}:${keySecret}`;
return `Basic ${Buffer.from(credentials).toString('base64')}`;
// Note: For production, consider Sinch OAuth for better security & rotation
// See: https://developers.sinch.com/docs/conversation/authentication/
}
export async function POST(request) {
const { to, text } = await request.json();
// --- 1. Input Validation ---
if (!to || !text) {
return NextResponse.json({ error: 'Missing ""to"" phone number or ""text"" message.' }, { status: 400 });
}
// Basic E.164 format check (adjust regex as needed for stricter validation)
if (!/^\+\d{11,15}$/.test(to)) {
return NextResponse.json({ error: 'Invalid ""to"" phone number format. Use E.164 (e.g., +15551234567).' }, { status: 400 });
}
const projectId = process.env.SINCH_PROJECT_ID;
const sinchAppId = process.env.SINCH_APP_ID;
const senderId = process.env.SINCH_WHATSAPP_SENDER_ID; // Used in payload, ensure it's set
const region = process.env.SINCH_REGION || 'us'; // Default to 'us' if not set
if (!projectId || !sinchAppId || !senderId) {
return NextResponse.json({ error: 'Sinch Project ID, App ID, or Sender ID not configured.' }, { status: 500 });
}
const sinchApiUrl = `https://${region}.conversation.api.sinch.com/v1/projects/${projectId}/messages:send`;
const payload = {
app_id: sinchAppId,
recipient: {
contact_id: to // Using contact_id for direct E.164 number sending
// Alternatively, use channel_identity for specific channel/number pair:
// channel_identity: { channel: 'WHATSAPP', identity: to }
},
message: {
text_message: {
text: text
}
// For Template Messages, use:
// template_message: {
// template_id: ""your_template_id"", // The registered template name/ID
// version: ""your_template_version"", // e.g., ""1""
// language_code: ""en_US"", // Or relevant language
// parameters: { /* template parameters if any */ }
// }
},
channel_priority_order: [""WHATSAPP""] // Ensure WhatsApp is prioritized
};
try {
// --- 2. API Call to Sinch ---
const response = await fetch(sinchApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': getSinchAuthToken() // Use Basic Auth or OAuth
},
body: JSON.stringify(payload),
});
const responseData = await response.json();
// --- 3. Response Handling ---
if (!response.ok) {
console.error('Sinch API Error:', response.status, responseData);
return NextResponse.json(
{ error: 'Failed to send message via Sinch.', details: responseData },
{ status: response.status }
);
}
console.log('Sinch Send Success:', responseData);
return NextResponse.json({ success: true, messageId: responseData.message_id });
} catch (error) {
// --- 4. Error Handling ---
console.error('Error sending WhatsApp message:', error);
return NextResponse.json({ error: 'Internal server error.' }, { status: 500 });
}
}
Explanation:
- Input Validation: Ensures the required
to
(recipient phone number in E.164 format) andtext
fields are present in the request body. Basic E.164 format validation is included. - Authentication: The
getSinchAuthToken
function generates the Basic Authentication header required by the Sinch API using your Key ID and Secret. Note: For production, consider implementing Sinch's OAuth 2.0 flow for more secure, short-lived tokens. - Payload Construction: Builds the JSON payload according to the Sinch Conversation API
/messages:send
endpoint specification. It includes the target app, recipient (usingcontact_id
for simplicity, assuming E.164 is sufficient), the message content (text_message
), and prioritizes theWHATSAPP
channel. Comments show how to structure atemplate_message
if needed (required outside the 24-hour window or for initiating conversations). - API Call: Uses the native
fetch
API to send the POST request to the appropriate regional Sinch API endpoint. - Response Handling: Checks if the API call was successful (
response.ok
). If not, logs the error details returned by Sinch and sends an appropriate error response back to the client. If successful, logs the success and returns themessage_id
. - Error Handling: A
try...catch
block handles network errors or other unexpected issues during the process.
3. Building the API Layer: Receiving Webhooks
To receive incoming messages and delivery status updates, Sinch needs a publicly accessible endpoint to send POST requests (webhooks).
File: app/api/sinch-webhook/route.js
// app/api/sinch-webhook/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
try {
const payload = await request.json();
console.log(`Received Sinch Webhook`);
console.log('Payload:', JSON.stringify(payload, null, 2)); // Log the full payload for inspection
// --- 1. Identify Event Type & Process based on Payload Structure ---
// Sinch uses different payload structures for different events.
// You MUST inspect payloads in the Sinch Dashboard or your logs to handle each relevant case.
if (payload.message_inbound_event) {
// --- 2. Handle Incoming Messages ---
const event = payload.message_inbound_event;
const message = event.message;
const contactId = event.contact_id; // Sender's phone number (usually)
const appId = event.app_id;
console.log(`---> Incoming Message from ${contactId} in App ${appId}`);
if (message.text_message) {
console.log(` Text: ${message.text_message.text}`);
// TODO: Add your logic here:
// - Store the message in a database
// - Trigger automated replies
// - Forward to a support system
} else if (message.media_message) {
console.log(` Media URL: ${message.media_message.url}`);
// TODO: Handle incoming media (images, videos, etc.)
}
// Add handling for other message types (location, contacts, etc.) as needed
} else if (payload.message_delivery_event) {
// --- 3. Handle Delivery Receipts ---
const event = payload.message_delivery_event;
const messageId = event.message_id;
const status = event.status; // e.g., DELIVERED, FAILED, READ
const reason = event.reason; // Reason for failure, if any
console.log(`---> Delivery Status for Message ${messageId}: ${status}`);
if (reason) {
console.log(` Reason: ${reason}`);
}
// TODO: Update message status in your database
} else {
// Log other event types you might receive but aren't explicitly handling yet
const eventType = Object.keys(payload)[0]; // Basic way to guess event type key
console.warn(`Received unhandled webhook event type: ${eventType || 'Unknown'}`);
}
// --- 4. Acknowledge Receipt ---
// Respond quickly to Sinch to acknowledge receipt.
// Process heavy logic asynchronously (e.g., using queues or background jobs).
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error processing Sinch webhook:', error);
// Avoid sending detailed errors back in the webhook response
return NextResponse.json({ error: 'Webhook processing failed.' }, { status: 500 });
}
}
// --- Optional: GET handler for verification ---
// Some services require a GET request check during webhook setup.
// Sinch doesn't typically require this, but it can be useful for health checks.
export async function GET(request) {
console.log('Received GET request on webhook endpoint.');
return NextResponse.json({ message: 'Sinch webhook endpoint is active.' });
}
Explanation:
- Receive Payload: The function parses the incoming JSON payload sent by Sinch.
- Event Type Identification: It checks the structure of the payload (e.g., presence of
message_inbound_event
ormessage_delivery_event
keys) to determine the event type. You must inspect actual webhook payloads received from Sinch (viewable in the Sinch Dashboard or your logs) to confirm the exact structure and event types you need to handle. - Incoming Message Handling: If it's an inbound message, it extracts key information like the sender (
contact_id
), the message content (text_message
,media_message
, etc.), and logs it. This is where you integrate your application logic – saving messages, triggering replies, etc. - Delivery Receipt Handling: If it's a delivery event, it extracts the message ID, status (
DELIVERED
,FAILED
,READ
), and potentially a failure reason. This is useful for tracking message status. - Acknowledgement: Crucially, the function returns a
200 OK
response (NextResponse.json({ success: true })
) quickly. Sinch expects a timely acknowledgement. Any time-consuming processing should be done asynchronously (e.g., using a job queue like BullMQ, or triggering serverless functions) to prevent webhook timeouts. - Error Handling: Catches errors during processing and logs them, returning a generic
500
error to Sinch. Avoid sending detailed internal error messages back in the response. - GET Handler (Optional): A simple GET handler is included. While Sinch might not use it for verification, it's helpful for manually checking if the endpoint is reachable and deployed correctly.
Security Note: For production, implementing webhook signature verification is strongly recommended to ensure that incoming requests genuinely originate from Sinch. Sinch likely provides a mechanism (e.g., using a secret key to generate an HMAC signature included in request headers). You must consult the official Sinch Conversation API documentation for the specific details on how to implement webhook signature verification and add the necessary validation logic to this webhook handler. This guide omits the specific code due to variance in implementation details, but it is a critical security step.
4. Integrating with Sinch: Configuration Steps
This section details how to obtain the necessary credentials and configure your Sinch App. Remember to replace YOUR_...
placeholders in .env.local
with the actual values you obtain here.
4.1 Obtain Sinch Credentials
- Log in: Access the Sinch Customer Dashboard.
- Project ID: Navigate to
""Settings""
(usually bottom-left) ->""Projects""
. Your Project ID is listed there. Copy this toSINCH_PROJECT_ID
in your.env.local
. - API Keys (Key ID & Secret):
- Go to
""Settings""
->""API Keys""
. - Click
""CREATE KEY""
. - Give it a descriptive name (e.g.,
""NextJS WhatsApp App Key""
). - Important: Copy the Key ID and Key Secret immediately and store them securely in your
.env.local
(SINCH_KEY_ID
,SINCH_KEY_SECRET
). The Key Secret is only shown once.
- Go to
- Conversation API Access & App:
- Navigate to
""Conversation API""
in the left menu. If you haven't already, click""GET ACCESS""
and agree to the terms. - Click
""CREATE APP""
. - Give your app a name (e.g.,
""My NextJS WhatsApp App""
). - Select the appropriate Region (e.g., US, EU, BR). This must match the
SINCH_REGION
in your.env.local
. - Click
""CREATE""
. - On the app's overview page, find the App ID. Copy this to
SINCH_APP_ID
in your.env.local
.
- Navigate to
- WhatsApp Sender ID & Bearer Token:
- This requires completing the WhatsApp Embedded Signup process within the Sinch Dashboard. Follow Sinch's guide: How do I use the WhatsApp embedded signup process? (or equivalent up-to-date documentation).
- During this process, you will link a phone number and obtain:
- WhatsApp Sender ID: This is usually the phone number in E.164 format (e.g.,
+15551234567
). Copy this toSINCH_WHATSAPP_SENDER_ID
. - Bearer Token (Access Token): A token specific to this WhatsApp Sender. Copy this to
SINCH_WHATSAPP_BEARER_TOKEN
. This token is needed for linking the sender in the dashboard setup.
- WhatsApp Sender ID: This is usually the phone number in E.164 format (e.g.,
4.2 Configure Sinch Conversation API App
- Navigate to your App: In the Sinch Dashboard, go to
""Conversation API""
->""Your Apps""
and select the app you created. - Add WhatsApp Channel:
- Scroll down to the
""Channels""
section. - Find
""WhatsApp""
and click""SET UP CHANNEL""
. - Select the Sender Identity (your WhatsApp Sender ID / phone number) from the dropdown list that you provisioned earlier.
- The
Bearer Token
associated with that Sender ID should be automatically used or confirmed here as part of the channel configuration. - Click
""SAVE""
.
- Scroll down to the
- Configure Webhooks:
- Scroll to the
""Webhooks""
section. - Click
""ADD WEBHOOK""
. - Target URL: Enter the publicly accessible URL for your webhook handler. This should be the value of
NEXT_PUBLIC_APP_URL
from your environment variables, followed by the API route path:${NEXT_PUBLIC_APP_URL}/api/sinch-webhook
.- For local development, use
ngrok
. Startngrok http 3000
and use the providedhttps://
URL (e.g.,https://your-ngrok-subdomain.ngrok-free.app/api/sinch-webhook
). UpdateNEXT_PUBLIC_APP_URL
in.env.local
temporarily. - For production (e.g., Vercel), use the deployment URL (e.g.,
https://your-app.vercel.app/api/sinch-webhook
). EnsureNEXT_PUBLIC_APP_URL
is set correctly in your deployment environment variables.
- For local development, use
- Triggers: Select the events you want to receive. At a minimum, choose:
MESSAGE_INBOUND
(for incoming messages)MESSAGE_DELIVERY
(for delivery receipts)
- You might add others like
CONTACT_CREATE_EVENT
,CONVERSATION_START_EVENT
, etc., based on your needs. - Secret: You can optionally add a secret token here. If you do, you must implement signature verification logic in your
sinch-webhook
route using this secret (refer to Sinch documentation). - Click
""ADD""
.
- Scroll to the
Follow the dashboard navigation paths precisely to find these settings.
5. Error Handling, Logging, and Retry Mechanisms
Robust error handling is crucial for production systems.
Error Handling Strategy:
- API Routes: Use
try...catch
blocks in bothsend-whatsapp
andsinch-webhook
routes. - Validation: Validate input early (request bodies, phone number formats).
- Sinch API Responses: Check
response.ok
and parse error details from the JSON body returned by Sinch on failure. Log these details. - Webhook Responses: Return
200 OK
quickly. For processing errors, log internally and return a generic500
error to Sinch without revealing internal details.
Logging:
- Use
console.log
,console.warn
, andconsole.error
strategically. - Log key events: sending requests, receiving webhooks, successful operations, errors, and relevant data (like message IDs, contact IDs, error messages from Sinch).
- In production, use a dedicated logging service (like Vercel Logs, Datadog, Sentry) for structured logging, searching, and alerting.
- Example Log Points: Added
console.log/error/warn
in the route handlers (Sections 2 & 3).
Retry Mechanisms (Basic):
-
For transient network errors or temporary Sinch API issues (e.g., 5xx errors) when sending messages, implement a simple retry strategy.
-
Caution: Avoid retrying blindly for client-side errors (4xx errors like invalid input). Retry primarily for server-side issues (5xx errors) or network timeouts.
-
Example (Conceptual - using
async-retry
library):First, install the library:
npm install async-retry
Then, integrate it into your sending logic:
// Inside app/api/send-whatsapp/route.js (conceptual integration) import { NextResponse } from 'next/server'; import retry from 'async-retry'; // ... getSinchAuthToken function ... export async function POST(request) { // ... input validation ... // ... variable setup (projectId, sinchApiUrl, payload) ... try { // Wrap the fetch call within the retry block const result = await retry( async (bail, attempt) => { console.log(`Attempting Sinch API call (attempt ${attempt})...`); const response = await fetch(sinchApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': getSinchAuthToken() }, body: JSON.stringify(payload), }); if (!response.ok) { const status = response.status; // Don't retry on client errors (4xx) if (status >= 400 && status < 500) { // Use bail to stop retrying and propagate the error immediately const errorData = await response.json(); const clientError = new Error(`Sinch API client error: ${status}`); clientError.originalStatus = status; clientError.details = errorData; bail(clientError); // Stop retrying return; // Bail prevents further execution here } // For server errors (5xx) or other network issues_ throw to trigger retry const serverError = new Error(`Sinch API server error or network issue: ${status}`); serverError.originalStatus = status; try { serverError.details = await response.json(); } catch { /* ignore parsing error */ } throw serverError; // Trigger retry } // If response is OK_ parse and return the data const responseData = await response.json(); console.log('Sinch Send Success within retry block:'_ responseData); return responseData; // Return successful data }_ { retries: 3_ // Number of retries factor: 2_ // Exponential backoff factor minTimeout: 1000_ // Initial timeout in ms (1 second) onRetry: (error_ attempt) => { console.warn(`Retrying Sinch API call (attempt ${attempt}) due to: ${error.message}`); } } ); // If retry succeeded, 'result' holds the responseData return NextResponse.json({ success: true, messageId: result.message_id }); } catch (error) { // This catches errors after all retries failed, or if bail was called console.error('Error sending WhatsApp message after retries:', error.message); // Log details if available (e.g., from clientError or last serverError) const status = error.originalStatus || 500; const details = error.details || null; return NextResponse.json( { error: 'Failed to send message via Sinch after retries.', details: details }, { status: status } ); } }
Integrate this retry logic carefully into your existing
send-whatsapp
route, replacing the simplefetch
call.
6. Database Schema and Data Layer (Optional)
While not strictly required for basic sending/receiving, storing message history or contact information is common. Using Prisma is a popular choice with Next.js.
6.1 Setup Prisma (Example)
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider postgresql # Or your preferred DB
Configure your database connection URL in .env
.
6.2 Example Schema
File: prisma/schema.prisma
// prisma/schema.prisma
datasource db {
provider = ""postgresql"" // Or ""mysql"", ""sqlite"", ""sqlserver"", ""mongodb""
url = env(""DATABASE_URL"")
}
generator client {
provider = ""prisma-client-js""
}
model Contact {
id String @id @default(cuid())
phone String @unique // E.164 format
name String?
createdAt DateTime @default(now())
messages Message[]
}
model Message {
id String @id @default(cuid())
sinchMessageId String? @unique // ID from Sinch API response/webhook
contactId String
contact Contact @relation(fields: [contactId], references: [id])
appId String? // Sinch App ID associated with the message
direction String // ""INBOUND"" or ""OUTBOUND""
channel String // e.g., ""WHATSAPP""
status String? // e.g., ""SENT"", ""DELIVERED"", ""READ"", ""FAILED"", ""RECEIVED""
content Json // Store text, media URLs, templates etc.
timestamp DateTime // Time of the event (from Sinch or DB creation)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reason String? // Failure reason from delivery receipt
}
6.3 Apply Schema & Generate Client
npx prisma migrate dev --name init # Creates migration and applies to DB
npx prisma generate # Generates Prisma Client
6.4 Usage (Conceptual in API Routes)
// Example in app/api/sinch-webhook/route.js
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// ... inside POST function, after parsing payload ...
if (payload.message_inbound_event) {
const event = payload.message_inbound_event;
const messageData = event.message;
const contactPhone = event.contact_id; // Assuming this is the E.164 phone
const sinchMsgId = event.message_id; // Check if inbound events provide a unique ID
// Perform DB operations asynchronously (move outside the immediate response if heavy)
try {
await prisma.message.create({
data: {
sinchMessageId: sinchMsgId, // Ensure this is unique or handle conflicts
contact: {
connectOrCreate: {
where: { phone: contactPhone },
create: { phone: contactPhone },
},
},
appId: event.app_id,
direction: 'INBOUND',
channel: event.channel, // Make sure channel is in the event payload
content: messageData, // Store the whole message object
timestamp: new Date(event.accepted_time), // Use Sinch timestamp
status: 'RECEIVED', // Custom status
},
});
console.log(`Saved inbound message ${sinchMsgId} from ${contactPhone}`);
} catch (dbError) {
console.error('Database error saving inbound message:', dbError);
// Decide how to handle DB errors - perhaps queue for later retry?
}
} else if (payload.message_delivery_event) {
const event = payload.message_delivery_event;
const sinchMsgId = event.message_id;
// Perform DB operations asynchronously
try {
const updatedMessage = await prisma.message.update({
where: { sinchMessageId: sinchMsgId },
data: {
status: event.status,
reason: event.reason,
timestamp: new Date(event.processed_time), // Use Sinch timestamp
},
});
console.log(`Updated status for message ${sinchMsgId} to ${event.status}`);
} catch (dbError) {
// Handle cases where the message might not exist (e.g., if outbound save failed)
if (dbError.code === 'P2025') { // Prisma code for RecordNotFound
console.warn(`Message with sinchMessageId ${sinchMsgId} not found for status update.`);
} else {
console.error('Database error updating message status:', dbError);
}
// Decide how to handle DB errors
}
}
// Remember to handle prisma client connection properly (e.g., singleton pattern)
// https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices