code examples

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

Sinch WhatsApp Integration Next.js Tutorial: Send & Receive Messages

Learn how to integrate Sinch WhatsApp Business API with Next.js. Complete step-by-step tutorial covering message sending, webhooks, authentication, and production deployment with the Conversation API.

How to Integrate Sinch WhatsApp with Next.js: Complete Developer Guide

Build robust WhatsApp messaging into your Next.js application using the Sinch Conversation API. This tutorial shows you how to send and receive WhatsApp messages, handle webhooks, manage security, and deploy a production-ready solution.

You'll create a Next.js application with API routes that interact with the Sinch Conversation API to send outbound WhatsApp messages and process inbound messages via webhooks. By the end, you'll have a functional bidirectional integration ready for testing and deployment.

What Will You Build with This Tutorial?

Objective: Implement WhatsApp messaging within your Next.js application to send outbound messages and receive inbound messages via the Sinch Conversation API.

Problem Solved: Programmatically communicate with users on WhatsApp for notifications, customer support, alerts, order updates, and marketing – directly from your web application.

Technologies & Requirements (Verified January 2025):

  • Next.js 13+: React framework for full-stack web applications. This guide uses the App Router (introduced in Next.js 13, stable in Next.js 14). Use Next.js 14 or Next.js 15 for the latest features and stability. The App Router provides modern API routes with improved performance.
  • Node.js 18.18.0+: JavaScript runtime environment. Use Node.js 20 LTS "Iron" (Maintenance LTS until April 2026) or Node.js 22 LTS "Jod" (Active LTS until October 2027). Node.js 18 LTS "Hydrogen" ended support in March 2025.
  • Sinch Conversation API: Unified API for managing conversations across multiple channels, including WhatsApp Business API. Includes template messages, media handling, and delivery tracking.
  • WhatsApp Business API: Meta's official business messaging platform. Requires an approved WhatsApp Business Account through Sinch's embedded signup. WhatsApp enforces a 24-hour messaging window for session messages. Use template messages to initiate conversations or message users outside this window.

System Architecture:

  1. End user's WhatsApp client communicates with the WhatsApp Business API platform
  2. WhatsApp platform relays messages to and from Sinch Conversation API
  3. Your Next.js application interacts with Sinch API to send messages (via HTTP) and receive messages/events (via webhooks)
  4. Your application logic triggers outbound messages and optionally stores message history in a database

Final Outcome – A Next.js Application With:

  1. API endpoint (/api/send-whatsapp) to send WhatsApp messages via Sinch
  2. API endpoint (/api/sinch-webhook) to receive incoming message notifications from Sinch
  3. Secure credential handling and comprehensive error handling
  4. Production deployment instructions and testing guidance

Prerequisites:

  • Node.js 18.18.0+ (LTS version recommended) and npm or yarn installed. Verify with node --version
  • Sinch account with Conversation API access. Enable postpay billing to send messages beyond initial free credits or for production volumes. Trial accounts work for initial testing within limits. Contact Sinch support for billing details.
  • Configured Sinch Conversation API App (covered in this guide)
  • Provisioned WhatsApp Business Sender ID via Sinch using Embedded Signup process
  • Familiarity with JavaScript, React, Next.js App Router, and REST APIs
  • Understanding of E.164 phone number format (international format with + prefix, e.g., +15551234567)

Technology Verification Date: All technology versions and requirements verified as of January 2025. Check official documentation for latest releases.

How Do You Set Up Your Next.js Project for Sinch WhatsApp?

Initialize your Next.js project and configure environment variables for Sinch integration.

Creating Your Next.js App with App Router

Open your terminal and run:

bash
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

How to Configure Sinch Environment Variables Securely

Sinch requires several credentials. Store them securely in environment variables. Create a .env.local file in your project root.

Never commit .env.local to version control. Add it to .gitignore if it's not already there.

Important: Replace all YOUR_... placeholders with your actual credentials from the Sinch dashboard (explained later).

plaintext
# .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: Store sensitive credentials outside your codebase for security. Next.js automatically loads .env.local variables into process.env.
  • Obtaining Values: The "Get Your Sinch Credentials" section explains how to find these values in the Sinch Customer Dashboard.
  • NEXT_PUBLIC_APP_URL: Set this to your application's publicly accessible base URL. Sinch uses this to send webhooks. The NEXT_PUBLIC_ prefix follows Next.js convention.

Understanding the Next.js App Router Project Structure

You'll work primarily in the app/api/ directory for 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: Next.js API routes keep backend logic colocated with your frontend, simplifying development and deployment. The App Router provides a modern, streamlined approach.

How Do You Send WhatsApp Messages with Sinch Conversation API?

Create the API endpoint that sends outbound WhatsApp messages via Sinch.

Understanding WhatsApp Message Types

WhatsApp enforces specific rules for message types:

Message TypeUse CaseRequirements
Session Messages (text_message)Reply within 24 hours of user's last messageNo pre-approval needed; allows free-form text and media
Template Messages (template_message)Initiate conversations or message outside 24-hour windowMust be pre-approved by WhatsApp; strict formatting with placeholders

When to use each type:

  • Use text_message for ongoing conversations where the user messaged you recently (within 24 hours)
  • Use template_message to:
    • Start new conversations
    • Send scheduled reminders
    • Reach users who haven't messaged in 24+ hours
    • Send notifications, alerts, or marketing (with user opt-in)

File: app/api/send-whatsapp/route.js

javascript
// app/api/send-whatsapp/route.js
import { NextResponse } from 'next/server';

// Basic Authentication Token Generation (cache 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 });
    }
}

Code Walkthrough:

  1. Input Validation: Validates that to (recipient phone number in E.164 format) and text fields exist in the request body. Includes basic E.164 format checking.
  2. Authentication: The getSinchAuthToken function generates the Basic Authentication header using your Key ID and Secret. For production, implement Sinch's OAuth 2.0 flow for more secure, short-lived tokens.
  3. Payload Construction: Builds the JSON payload according to the Sinch Conversation API /messages:send endpoint specification. Includes the target app, recipient (using contact_id), message content (text_message), and prioritizes the WHATSAPP channel. Comments show how to structure a template_message (required outside the 24-hour window or for initiating conversations).
  4. API Call: Uses the native fetch API to send the POST request to the appropriate regional Sinch API endpoint.
  5. Response Handling: Checks if the API call succeeded (response.ok). Logs error details returned by Sinch on failure and sends an appropriate error response to the client. On success, logs the result and returns the message_id.
  6. Error Handling: A try...catch block handles network errors or unexpected issues during the process.

How Do You Receive WhatsApp Messages Using Sinch Webhooks?

Sinch needs a publicly accessible endpoint to send incoming messages and delivery status updates via POST requests (webhooks).

File: app/api/sinch-webhook/route.js

javascript
// 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.
        // 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's 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.' });
}

Code Walkthrough:

  1. Receive Payload: Parses the incoming JSON payload sent by Sinch.
  2. Event Type Identification: Checks the payload structure (e.g., presence of message_inbound_event or message_delivery_event keys) to determine the event type. 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.
  3. Incoming Message Handling: For inbound messages, extracts key information like the sender (contact_id), message content (text_message, media_message, etc.), and logs it. Integrate your application logic here – save messages, trigger replies, etc.
  4. Delivery Receipt Handling: For delivery events, extracts the message ID, status (DELIVERED, FAILED, READ), and potentially a failure reason. Use this for tracking message status.
  5. Acknowledgement: Returns a 200 OK response (NextResponse.json({ success: true })) quickly. Sinch expects timely acknowledgement. Move time-consuming processing to asynchronous operations (e.g., job queue like BullMQ, or serverless functions) to prevent webhook timeouts.
  6. Error Handling: Catches errors during processing and logs them, returning a generic 500 error to Sinch. Avoid sending detailed internal error messages.
  7. GET Handler (Optional): A simple GET handler for manually checking if the endpoint is reachable and deployed correctly.

Security Note: For production, implement webhook signature verification to ensure incoming requests genuinely originate from Sinch. Sinch likely provides a mechanism (e.g., using a secret key to generate an HMAC signature in request headers). Consult the official Sinch Conversation API documentation for specific details on implementing webhook signature verification and add the necessary validation logic to this webhook handler. This is a critical security step.

How to Get Your Sinch Credentials and Configure WhatsApp

Obtain the necessary credentials and configure your Sinch App. Replace YOUR_... placeholders in .env.local with the actual values you obtain here.

How to Obtain Sinch API Credentials

  1. Log in: Access the Sinch Customer Dashboard.
  2. Project ID: Navigate to "Settings" (usually bottom-left) → "Projects". Your Project ID is listed there. Copy this to SINCH_PROJECT_ID in your .env.local.
  3. 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.
  4. 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.
  5. WhatsApp Sender ID & Bearer Token:
    • Complete 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'll link a phone number and obtain:
      • WhatsApp Sender ID: Usually the phone number in E.164 format (e.g., +15551234567). Copy this to SINCH_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.

How to Configure Your Sinch Conversation API App for WhatsApp

  1. Navigate to your App: In the Sinch Dashboard, go to "Conversation API" → "Your Apps" and select the app you created.
  2. 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".
  3. 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. Start ngrok http 3000 and use the provided https:// URL (e.g., https://your-ngrok-subdomain.ngrok-free.app/api/sinch-webhook). Update NEXT_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). Ensure NEXT_PUBLIC_APP_URL is set correctly in your deployment environment variables.
    • Triggers: Select the events you want to receive. At a minimum, choose:
      • MESSAGE_INBOUND (for incoming messages)
      • MESSAGE_DELIVERY (for delivery receipts)
    • Add others like CONTACT_CREATE_EVENT, CONVERSATION_START_EVENT, etc., based on your needs.
    • Secret: Optionally add a secret token here. If you do, implement signature verification logic in your sinch-webhook route using this secret (refer to Sinch documentation).
    • Click "ADD".

Follow the dashboard navigation paths precisely to find these settings.

What Error Handling Best Practices Should You Implement?

Robust error handling is crucial for production WhatsApp messaging systems.

Error Handling Strategy

ComponentStrategy
API RoutesUse try...catch blocks in both send-whatsapp and sinch-webhook routes
ValidationValidate input early (request bodies, phone number formats)
Sinch API ResponsesCheck response.ok and parse error details from the JSON body returned by Sinch on failure. Log these details.
Webhook ResponsesReturn 200 OK quickly. For processing errors, log internally and return a generic 500 error to Sinch without revealing internal details.

Logging Best Practices

  • Use console.log, console.warn, and console.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.

Retry Mechanisms

For transient network errors or temporary Sinch API issues (e.g., 5xx errors) when sending messages, implement a 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 using async-retry library:

First, install the library:

bash
npm install async-retry

Then, integrate it into your sending logic:

javascript
// 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 simple fetch call.

Should You Store WhatsApp Message History in a Database?

While not required for basic sending/receiving, storing message history or contact information is common. Prisma is a popular choice with Next.js for database operations.

For a complete guide on implementing database storage with Prisma, including schema design and integration examples, see our Next.js database integration tutorial.

How to Set Up Prisma for Message Storage

bash
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.

Example Database Schema for WhatsApp Messages

File: prisma/schema.prisma

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
}

Apply Schema & Generate Client

bash
npx prisma migrate dev --name init # Creates migration and applies to DB
npx prisma generate              # Generates Prisma Client

Usage in API Routes

javascript
// 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
     }
}

// 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