code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Send SMS with Next.js and Sinch REST API: Complete Tutorial 2025

Build a Next.js application that sends SMS messages using the Sinch REST API. Step-by-step guide with API routes, authentication, Zod validation, error handling, and deployment.

Send SMS with Next.js and the Sinch REST API

Follow this step-by-step guide to build a Next.js application that sends SMS messages using the Sinch SMS REST API. You'll learn project setup, API integration, security best practices, and deployment.

<!-- DEPTH: Introduction lacks context about when to use Sinch vs alternatives and use case scenarios (Priority: Medium) -->

This guide focuses on sending outbound SMS messages triggered from a web interface. You'll use Next.js API Routes to securely handle communication with the Sinch API on the server-side.

Project Overview and Goals

What You'll Build:

  1. A simple Next.js application with:
  2. A frontend form to input recipient phone numbers and message text.
  3. A backend API route (/api/send-sms) that receives data from the form.
  4. Server-side logic within the API route to securely call the Sinch SMS REST API (/batches endpoint) and send messages.
  5. Clear feedback to users indicating success or failure.
<!-- GAP: Missing estimated time to complete tutorial (Type: Enhancement) --> <!-- DEPTH: "What You'll Build" section lacks information about scalability and production readiness (Priority: Medium) -->

Problem You'll Solve:

Send SMS messages programmatically from your web application without exposing sensitive API credentials to client-side browsers. You'll integrate secure SMS functionality – like notifications or alerts – into your Next.js project.

<!-- EXPAND: Could benefit from real-world use case examples (e.g., OTP, appointment reminders, marketing) (Type: Enhancement) -->

Technologies Used:

  • Next.js: A React framework for building full-stack web applications. You'll use it for both your frontend UI and backend API route.
  • React: Build your user interface components with React.
  • Sinch SMS REST API: Send SMS messages through this service. You'll interact with it directly via HTTP requests.
  • Node.js: The runtime environment for Next.js.
  • Zod (Optional but Recommended): Validate input robustly on the server-side.
<!-- GAP: Missing information about why Sinch specifically and its advantages (Type: Substantive) -->

System Architecture:

mermaid
graph LR
    A[User Browser (Frontend Form)] -- POST /api/send-sms (JSON payload) --> B(Next.js API Route);
    B -- Reads Env Vars (Credentials) --> C{Environment Variables};
    B -- POST Request (JSON payload + Auth) --> D(Sinch SMS REST API /batches);
    D -- SMS Sent --> E(Recipient Phone);
    D -- API Response (Success/Error) --> B;
    B -- API Response (Success/Error) --> A;

    style C fill:#f9f,stroke:#333,stroke-width:2px
<!-- DEPTH: Architecture diagram lacks explanation of data flow and security boundaries (Priority: Medium) -->

Prerequisites:

  • Node.js (version 22.x LTS recommended, minimum 18.x supported)
  • npm or yarn package manager
  • A Sinch account (sign up if you don't have one)
  • A provisioned Sinch phone number (or Alphanumeric Sender ID if applicable)
  • Your Sinch SERVICE_PLAN_ID and API_TOKEN (found in your Sinch Customer Dashboard under SMS > APIs)
  • Basic familiarity with JavaScript, React, and terminal/command line usage.
<!-- GAP: Missing information about Sinch account setup costs and free tier details (Type: Substantive) -->

Source: Node.js v22 'Jod' LTS (Active LTS until October 2025, Maintenance LTS until April 2027)

Final Outcome:

By the end of this guide, you'll have a functional Next.js application that securely sends SMS messages to specified phone numbers using your Sinch credentials.

<!-- DEPTH: Final outcome lacks information about what to do next after tutorial (Priority: Low) -->

1. Setting Up the Project

Start by creating a new Next.js project and setting up the necessary configurations.

  1. Create a Next.js App: Open your terminal and run the following command. Replace sinch-sms-app with your desired project name.

    bash
    npx create-next-app@latest sinch-sms-app

    You'll be prompted with configuration questions. For this guide, the following selections are recommended:

    • Would you like to use TypeScript? No (or Yes, if you prefer)
    • Would you like to use ESLint? Yes
    • Would you like to use Tailwind CSS? No (Keep it simple for this example)
    • Would you like to use src/ directory? No (or Yes, adjust paths accordingly)
    • Would you like to use App Router? (recommended) No (We'll use the Pages Router for this API route example)
    • Would you like to customize the default import alias? No

    Note on Router Choice: This guide uses the Pages Router for simpler API route implementation. While Next.js 15 (released October 2024) recommends the App Router as the default, the Pages Router remains fully supported and is ideal for straightforward API endpoints. Both routers can coexist in the same application if needed.

    Source: Next.js 15 Official Documentation (2024-2025)

<!-- EXPAND: Could benefit from a comparison table of Pages Router vs App Router for API routes (Type: Enhancement) -->
  1. Navigate into Project Directory:

    bash
    cd sinch-sms-app
  2. Install Validation Library (Recommended): Use Zod to validate incoming request data in your API route.

    bash
    npm install zod

    Why Zod? It provides a clear, type-safe way to define schemas and validate data, preventing invalid requests from reaching the Sinch API and improving security. Zod v4 (released 2025) offers 14x faster string parsing and a 57% smaller core compared to v3, making it highly efficient for production use.

    Source: Zod v4 Release Notes (2025)

<!-- GAP: Missing information about alternatives to Zod (e.g., Joi, Yup) and trade-offs (Type: Enhancement) -->
  1. Set Up Environment Variables: Sensitive credentials like API keys must never be committed to your code repository. Use environment variables to protect them.

    • Create a file named .env.local in the root of your project directory.

    • Add the following lines to .env.local, replacing the placeholder values with your actual Sinch credentials:

      plaintext
      # .env.local
      
      # Found in Sinch Dashboard -> SMS -> APIs -> REST configuration
      SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID
      SINCH_API_TOKEN=YOUR_API_TOKEN
      
      # A phone number associated with your Sinch account/Service Plan ID
      # Must be in E.164 format (e.g., +12025550183)
      SINCH_NUMBER=+YOUR_SINCH_PHONE_NUMBER_E164
      
      # The base URL for the Sinch SMS API region your account uses
      # Common examples:
      # US: https://us.sms.api.sinch.com
      # EU: https://eu.sms.api.sinch.com
      # Check Sinch docs for other regions if needed
      SINCH_REGION_URL=https://us.sms.api.sinch.com
    • Obtaining Credentials:

      • Log in to your Sinch Customer Dashboard.
      • Navigate to SMS in the left-hand menu, then click APIs.
      • Under REST configuration, you will find your SERVICE_PLAN_ID.
      • Click Show next to API_TOKEN to reveal your token. Copy both securely.
      • Your assigned SINCH_NUMBER can often be found by clicking on the SERVICE_PLAN_ID link on the same page and scrolling down, or under the Numbers section of the dashboard. Ensure it's linked to the correct Service Plan ID.
      • Confirm the correct SINCH_REGION_URL for your account.
<!-- DEPTH: Credentials section lacks screenshots or visual guide for finding credentials (Priority: Medium) --> <!-- GAP: Missing information about credential rotation best practices (Type: Substantive) --> * **Security:** The `.env.local` file is listed in Next.js's default `.gitignore` file, preventing accidental commits. Variables defined here are only available on the server-side (like in API routes), not exposed to the client browser.

5. Project Structure Overview: For this guide using the Pages Router, the key files/folders will be: * pages/index.js: Our main frontend page with the form. * pages/api/send-sms.js: Our backend API endpoint. * .env.local: Stores our secret credentials. * package.json: Lists project dependencies.

<!-- EXPAND: Could benefit from complete project directory tree visualization (Type: Enhancement) -->

2. Implementing Core Functionality (API Route)

Create the server-side API route that handles requests from your frontend and interacts with the Sinch API.

  1. Create the API Route File: Create a new file at pages/api/send-sms.js.

  2. Implement the API Logic: Paste the following code into pages/api/send-sms.js:

    javascript
    // pages/api/send-sms.js
    import { z } from 'zod';
    
    // Define the expected shape of the request body using Zod
    const SendSmsSchema = z.object({
      // Basic E.164 format validation (adjust regex as needed for stricter validation)
      recipient: z.string().regex(/^\+[1-9]\d{1,14}$/, "Invalid phone number format. Use E.164 format (e.g., +12025550183)."),
      message: z.string().min(1, "Message cannot be empty.").max(1600, "Message too long."), // Max length adjusted for multi-part messages
    });
    
    export default async function handler(req, res) {
      // Only allow POST requests
      if (req.method !== 'POST') {
        res.setHeader('Allow', ['POST']);
        return res.status(405).json({ error: `Method ${req.method} Not Allowed` });
      }
    
      // --- 1. Input Validation ---
      let validatedData;
      try {
        validatedData = SendSmsSchema.parse(req.body);
      } catch (error) {
        console.error("Validation Error:", error.errors);
        // Return specific validation errors to the client
        return res.status(400).json({ error: "Invalid input.", details: error.format() });
      }
    
      const { recipient, message } = validatedData;
    
      // --- 2. Retrieve Credentials Securely ---
      const servicePlanId = process.env.SINCH_SERVICE_PLAN_ID;
      const apiToken = process.env.SINCH_API_TOKEN;
      const sinchNumber = process.env.SINCH_NUMBER; // This is the 'from' number
      const sinchRegionUrl = process.env.SINCH_REGION_URL;
    
      if (!servicePlanId || !apiToken || !sinchNumber || !sinchRegionUrl) {
        console.error("Sinch API credentials or configuration missing in environment variables.");
        return res.status(500).json({ error: "Server configuration error." });
      }
    
      // --- 3. Construct Sinch API Request ---
      const sinchEndpoint = `${sinchRegionUrl}/xms/v1/${servicePlanId}/batches`;
      // We use the /batches endpoint as it's the standard Sinch REST API endpoint for sending one or more messages.
    
      const payload = {
        from: sinchNumber, // Uses the number from .env.local
        to: [recipient], // API expects an array of recipients
        body: message,
        // You can add more parameters here if needed (e.g., delivery_report)
        // delivery_report: ""full""
      };
    
      // --- 4. Make the API Call to Sinch ---
      try {
        console.log(`Sending SMS via Sinch to: ${recipient}`);
        const response = await fetch(sinchEndpoint, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${apiToken}` // Use Bearer token authentication
          },
          body: JSON.stringify(payload),
        });
    
        // --- 5. Handle Sinch API Response ---
        if (!response.ok) {
          // Attempt to read error details from Sinch response
          let errorDetails = await response.text(); // Read as text first
          try {
            errorDetails = JSON.parse(errorDetails); // Try parsing as JSON
          } catch (parseError) {
            // Keep as text if not valid JSON
            console.warn("Could not parse Sinch error response as JSON.");
          }
    
          console.error(`Sinch API Error (${response.status}):`, errorDetails);
          // Return a generic server error, but log specific details
          return res.status(500).json({
            error: `Failed to send SMS. Sinch API responded with status ${response.status}.`,
            sinchError: errorDetails // Optionally include Sinch error in dev/staging
          });
        }
    
        const data = await response.json();
        console.log("Sinch API Success:", data);
    
        // Respond to the client application
        return res.status(200).json({ success: true, message: "SMS submitted successfully.", details: data });
    
      } catch (error) {
        console.error("Error sending SMS request:", error);
        return res.status(500).json({ error: "Internal Server Error.", details: error.message });
      }
    }
<!-- DEPTH: Code lacks inline comments explaining business logic decisions (Priority: Low) --> <!-- GAP: Missing discussion of webhook setup for delivery reports (Type: Substantive) -->

Code Explanation:

  1. Import Zod: Import the zod library for validation.

  2. Define Schema: SendSmsSchema defines the expected structure (recipient, message) and validation rules (E.164 format for phone, non-empty message).

    • E.164 Format: The regex /^\+[1-9]\d{1,14}$/ validates international phone numbers according to the ITU-T E.164 standard. E.164 numbers must start with a + sign, followed by 1–3 digit country code and subscriber number, with a maximum total of 15 digits. The format excludes spaces, parentheses, or dashes (e.g., +14151231234 for a US number).

    Source: ITU-T E.164 Recommendation – The International Public Telecommunication Numbering Plan

<!-- EXPAND: Could benefit from table of example E.164 formats by country (Type: Enhancement) -->
  1. Method Check: The handler function checks if the incoming request method is POST. If not, it returns a 405 Method Not Allowed error.
  2. Input Validation: Use SendSmsSchema.parse(req.body) inside a try...catch block. If validation fails, the code logs specific errors and returns a 400 Bad Request response to the client with details.
  3. Retrieve Credentials: Read the Sinch credentials and configuration safely from process.env. Check if they exist and return a 500 error if any are missing.
  4. Construct Request: Build the full Sinch API endpoint URL. The code explicitly mentions why it uses the /batches endpoint. Construct the JSON payload required by the /batches endpoint, including from (using the sinchNumber variable retrieved from environment variables), to (as an array), and body.
  5. Make API Call: Use the built-in fetch API to make a POST request to the sinchEndpoint.
    • Set Content-Type: application/json.
    • Authorization: Bearer ${apiToken} provides the authentication token. Sinch SMS API uses Bearer token authentication with the API_TOKEN.
    • Convert the payload to a JSON string in the request body.
  6. Handle Response:
    • Check response.ok (true for HTTP status codes 200–299).
    • If not ok: Log the Sinch error status and attempt to parse the error response body (which might contain useful details from Sinch) before returning a 500 error to the client.
    • If ok: Parse the successful JSON response from Sinch, log it, and return a 200 success response to the client.
  7. Catch Fetch Errors: A final try...catch block handles potential network errors during the fetch call itself, returning a 500 error.
<!-- GAP: Missing information about Sinch API response structure and batch ID tracking (Type: Substantive) -->

3. Building a Simple Frontend

Create a basic form on the homepage to trigger your API route.

  1. Modify the Homepage: Open pages/index.js and replace its contents with the following:

    javascript
    // pages/index.js
    import { useState } from 'react';
    import Head from 'next/head';
    import styles from '../styles/Home.module.css'; // Assuming default styles exist
    
    export default function Home() {
      const [recipient, setRecipient] = useState('');
      const [message, setMessage] = useState('');
      const [status, setStatus] = useState(''); // To display success/error messages
      const [isLoading, setIsLoading] = useState(false);
      const [errorDetails, setErrorDetails] = useState(null);
    
      const handleSubmit = async (event) => {
        event.preventDefault(); // Prevent default form submission
        setIsLoading(true);
        setStatus(''); // Clear previous status
        setErrorDetails(null); // Clear previous errors
    
        try {
          const response = await fetch('/api/send-sms', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({ recipient, message }),
          });
    
          const data = await response.json(); // Always parse JSON response
    
          if (!response.ok) {
            // Handle HTTP errors (4xx, 5xx)
            setStatus(`Error: ${data.error || `Request failed with status ${response.status}`}`);
            setErrorDetails(data.details || data.sinchError || 'No additional details.'); // Show validation or Sinch errors if available
            console.error("API Error Data:", data);
          } else {
            // Handle success (2xx)
            setStatus(`Success: ${data.message || 'SMS submitted!'}`);
            setRecipient(''); // Clear form on success
            setMessage('');
            console.log("API Success Data:", data);
          }
        } catch (error) {
          // Handle network errors or issues with fetch itself
          console.error('Form submission error:', error);
          setStatus(`Error: ${error.message || 'An unexpected error occurred.'}`);
        } finally {
          setIsLoading(false); // Re-enable button
        }
      };
    
      return (
        <div className={styles.container}>
          <Head>
            <title>Send SMS with Next.js & Sinch</title>
            <meta name=""description"" content=""Send SMS using Sinch API via Next.js"" />
            <link rel=""icon"" href=""/favicon.ico"" />
          </Head>
    
          <main className={styles.main}>
            <h1 className={styles.title}>
              Send SMS via Sinch
            </h1>
    
            <form onSubmit={handleSubmit} className={styles.form}>
              <div className={styles.formGroup}>
                <label htmlFor=""recipient"">Recipient Phone Number:</label>
                <input
                  type=""tel""
                  id=""recipient""
                  value={recipient}
                  onChange={(e) => setRecipient(e.target.value)}
                  placeholder=""+12025550183"" // E.164 format
                  required
                  disabled={isLoading}
                />
                 <small>Use E.164 format (e.g., +12025550183)</small>
              </div>
    
              <div className={styles.formGroup}>
                <label htmlFor=""message"">Message:</label>
                <textarea
                  id=""message""
                  value={message}
                  onChange={(e) => setMessage(e.target.value)}
                  placeholder=""Enter your SMS message here""
                  required
                  rows={4}
                  maxLength={1600} // Reflect API limit loosely
                  disabled={isLoading}
                />
              </div>
    
              <button type=""submit"" disabled={isLoading} className={styles.button}>
                {isLoading ? 'Sending...' : 'Send SMS'}
              </button>
            </form>
    
            {status && (
              <div className={`${styles.statusMessage} ${status.startsWith('Error') ? styles.error : styles.success}`}>
                <p>{status}</p>
                {errorDetails && <pre>{JSON.stringify(errorDetails, null, 2)}</pre>}
              </div>
            )}
    
          </main>
    
          {/* Basic Styling (Add to styles/Home.module.css or use inline styles) */}
          <style jsx>{`
            .form {
              display: flex;
              flex-direction: column;
              width: 100%;
              max-width: 400px;
              margin-top: 2rem;
            }
            .formGroup {
              display: flex;
              flex-direction: column;
              margin-bottom: 1rem;
            }
            .formGroup label {
              margin-bottom: 0.5rem;
              font-weight: bold;
            }
            .formGroup input,
            .formGroup textarea {
              padding: 0.75rem;
              border: 1px solid #ccc;
              border-radius: 4px;
              font-size: 1rem;
            }
             .formGroup small {
              font-size: 0.8rem;
              color: #555;
              margin-top: 0.25rem;
            }
            .button {
              padding: 0.75rem 1.5rem;
              background-color: #0070f3;
              color: white;
              border: none;
              border-radius: 4px;
              font-size: 1rem;
              cursor: pointer;
              transition: background-color 0.2s ease;
              margin-top: 1rem;
            }
            .button:disabled {
              background-color: #ccc;
              cursor: not-allowed;
            }
            .button:hover:not(:disabled) {
              background-color: #005bb5;
            }
            .statusMessage {
              margin-top: 1.5rem;
              padding: 1rem;
              border-radius: 4px;
              width: 100%;
              max-width: 400px;
              text-align: center;
            }
            .statusMessage.error {
              background-color: #f8d7da;
              color: #721c24;
              border: 1px solid #f5c6cb;
            }
             .statusMessage.error pre {
               margin-top: 0.5rem;
               font-size: 0.85rem;
               text-align: left;
               background-color: #f1f1f1;
               padding: 0.5rem;
               border-radius: 3px;
               white-space: pre-wrap; /* Wrap long lines */
               word-wrap: break-word; /* Break words if needed */
             }
            .statusMessage.success {
              background-color: #d4edda;
              color: #155724;
              border: 1px solid #c3e6cb;
            }
          `}</style>
        </div>
      );
    }
<!-- DEPTH: Frontend code lacks accessibility considerations (ARIA labels, keyboard navigation) (Priority: Medium) --> <!-- GAP: Missing client-side phone number validation and formatting (Type: Substantive) -->

Code Explanation:

  1. State Variables: Use useState to manage the recipient number, message text, loading state, status messages, and detailed error information.
  2. handleSubmit Function:
    • Prevents the default browser form submission.
    • Sets isLoading to true to disable the button and provide feedback.
    • Clears previous status/error messages.
    • Uses fetch to send a POST request to your /api/send-sms endpoint.
    • Includes the recipient and message state values in the JSON request body.
    • Crucially, it checks response.ok after parsing the JSON response (data) to handle both successful responses and structured error responses from your API route.
    • Updates the status and errorDetails state based on the response.
    • Clears the form fields on successful submission.
    • Catches potential network errors.
    • Sets isLoading back to false in the finally block.
  3. Form Elements: Standard HTML form elements (input for phone, textarea for message) bound to the React state variables. Includes basic HTML5 required validation. The submit button is disabled while isLoading is true.
  4. Status Display: Conditionally renders a div to show success or error messages based on the status state. It includes a <pre> tag to display formatted JSON error details if available.
  5. Styling: Uses CSS Modules by importing styles from '../styles/Home.module.css' and applying classes like styles.container. It assumes the default Home.module.css file generated by create-next-app contains basic layout styles. Additionally, it includes inline <style jsx> for specific form and status message styling for demonstration purposes. Modify styles/Home.module.css or choose a different styling approach (like Tailwind, if selected during setup).
<!-- EXPAND: Could benefit from adding character counter for message field (Type: Enhancement) -->

4. Integrating with Sinch (Recap)

This section serves as a checklist and recap for the Sinch-specific integration points:

  • Credentials (.env.local): Ensure SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, SINCH_NUMBER, and SINCH_REGION_URL are correctly set in your .env.local file.
  • Dashboard Navigation:
    • Go to Sinch Customer Dashboard.
    • SMS -> APIs: Find SERVICE_PLAN_ID and API_TOKEN.
    • SMS -> APIs -> Click SERVICE_PLAN_ID (or Numbers section): Find/confirm your assigned SINCH_NUMBER(s).
    • Confirm your account's region (e.g., US, EU) to set the correct SINCH_REGION_URL.
  • Authentication: The API route (pages/api/send-sms.js) correctly uses Authorization: Bearer YOUR_API_TOKEN in the fetch request header.
  • API Endpoint: The API route targets the correct Sinch endpoint: {SINCH_REGION_URL}/xms/v1/{SINCH_SERVICE_PLAN_ID}/batches.
  • Payload: The request body sent to Sinch matches the required format: { ""from"": ""YOUR_SINCH_NUMBER_E164"", ""to"": [""...""], ""body"": ""..."" }. The from field uses the value stored in the SINCH_NUMBER environment variable.
<!-- GAP: Missing information about testing credentials in Sinch dashboard (Type: Substantive) -->

5. Error Handling and Logging

Our API route includes basic error handling:

  • Input Validation: Zod catches invalid phone number formats or empty messages before calling Sinch, returning a 400 error with details.
  • Credential Check: Returns a 500 error if environment variables are missing.
  • Sinch API Errors: Catches non-2xx responses from Sinch. It logs the status and attempts to log the response body from Sinch on the server-side for debugging. It returns a 500 error to the client.
  • Fetch Errors: Catches network errors during the fetch call, returning a 500 error.
  • Logging: Uses console.log and console.error within the API route (server-side). In a production environment, you would replace this with a structured logging library (like Pino or Winston) and configure it to output JSON, often sending these logs to a dedicated service (e.g., Datadog, Logtail, Axiom). Consult the documentation for your chosen logging library and Next.js integration patterns.
  • Frontend Feedback: The frontend displays clear success or error messages, including details from validation or API errors when available.
<!-- DEPTH: Error handling section lacks specific error code examples and resolution steps (Priority: High) --> <!-- GAP: Missing information about setting up error alerting/monitoring (Type: Substantive) -->

Testing Error Scenarios:

  • Enter an invalid phone number format (e.g., ""12345"") -> Expect a 400 error with Zod details.
  • Leave the message blank -> Expect a 400 error.
  • Temporarily put an invalid SINCH_API_TOKEN in .env.local and restart the server -> Expect a 500 error on the frontend, and check server logs for a 401 Unauthorized error from Sinch.
  • Temporarily remove SINCH_SERVICE_PLAN_ID from .env.local and restart -> Expect a 500 error (Server configuration error).
<!-- EXPAND: Could benefit from a comprehensive error code reference table (Type: Enhancement) -->

Retry Mechanisms: For transient network issues or specific Sinch errors (like rate limiting - 429), implementing a retry strategy with exponential backoff on the server-side can improve reliability. This is more advanced and typically involves libraries like async-retry. For this basic guide, we are not implementing automatic retries.

<!-- GAP: Missing code example for implementing retry logic (Type: Substantive) -->

6. Database Schema and Data Layer

Not applicable for this simple ""send-only"" example. If you needed to store message history, recipient lists, or track statuses, you would introduce a database (like PostgreSQL, MongoDB) and a data access layer (using an ORM like Prisma or directly with a database driver).

<!-- EXPAND: Could benefit from example schema design for message tracking (Type: Enhancement) -->

7. Security Features

  • Environment Variables: Secrets (API_TOKEN, SERVICE_PLAN_ID) are stored securely in .env.local and accessed only server-side. Never commit .env.local to Git.
  • Server-Side API Calls: All interaction with the Sinch API happens within the Next.js API route, preventing exposure of credentials to the browser.
  • Input Validation (Zod): The API route validates and sanitizes input (recipient, message) before processing, mitigating risks like injection attacks targeting the Sinch API parameters (though less common for SMS content itself). It ensures the phone number format is plausible.
  • Method Restriction: The API route explicitly checks for the POST method, rejecting others.
  • Rate Limiting (Consideration): For production, implement rate limiting on the /api/send-sms endpoint to prevent abuse (e.g., flooding users with messages, exhausting your Sinch budget). You can use:
    • Vercel's built-in security features (depending on your plan).
    • Middleware with libraries like rate-limiter-flexible or @upstash/ratelimit. Search for guides on implementing rate limiting in Next.js API routes using these libraries.
  • CSRF Protection: Next.js has some built-in CSRF protection mechanisms, but ensure you understand how they apply, especially if modifying default form handling. Using standard API route calls initiated from the same origin is generally safe.
<!-- GAP: Missing code example for implementing rate limiting (Type: Critical) --> <!-- DEPTH: Security section lacks discussion of data privacy regulations (GDPR, TCPA) (Priority: High) --> <!-- GAP: Missing information about PII handling and data retention policies (Type: Critical) -->

8. Handling Special Cases

  • Phone Number Formatting: The guide enforces E.164 format (+countrycodeNUMBER) via Zod validation. This is crucial for international deliverability.

  • Message Length & Encoding: SMS messages use two primary encoding standards:

    • GSM-7 Encoding: Standard SMS alphabet supporting 160 characters per single message. For multi-part messages, each segment contains 153 characters (7 characters reserved for concatenation headers).
    • UCS-2 Encoding: Unicode encoding supporting 70 characters per single message (international characters, emojis). For multi-part messages, each segment contains 67 characters.
    • The Sinch API automatically handles encoding selection and message segmentation. Our Zod validation allows up to 1600 characters, accommodating multi-part messages, but be mindful that each segment incurs separate costs.
    • Important: Using special characters like curly quotes (" ") instead of straight quotes forces UCS-2 encoding, reducing capacity from 160 to 70 characters and potentially increasing costs significantly.

    Source: GSM 03.38 Standard, ITU-T SMS Specifications

<!-- EXPAND: Could benefit from table showing character limits by encoding type (Type: Enhancement) -->
  • International Sending: Ensure your Sinch account and number are enabled for sending to the desired destination countries. Costs may vary.
  • Alphanumeric Sender IDs: If configured and allowed in the destination country, you can replace SINCH_NUMBER with an Alphanumeric Sender ID (e.g., "MyCompany") in the from field of the payload. Update .env.local accordingly if using this.
<!-- GAP: Missing information about country-specific regulations and sender ID requirements (Type: Substantive) -->

9. Performance Optimizations

For this simple use case, performance is unlikely to be an issue. However, for high-volume sending:

  • Keep API Route Lean: Avoid heavy computations or blocking operations within the API route.
  • Asynchronous Processing: For very high throughput, consider offloading the Sinch API call to a background job queue (e.g., BullMQ, Vercel Queue, AWS SQS). The API route would simply add the job to the queue and return immediately, while a separate worker process handles the actual sending and retries.
  • Sinch API Concurrency: Be aware of any rate limits or concurrency limits imposed by Sinch on your account.
<!-- DEPTH: Performance section lacks benchmarks and scalability thresholds (Priority: Medium) --> <!-- GAP: Missing information about caching strategies (Type: Enhancement) -->

10. Monitoring, Observability, and Analytics

In a production setting:

  • Logging: Implement structured logging (JSON format) in the API route and ship logs to a centralized service (Datadog, Logtail, Better Stack, etc.) for analysis and alerting.
  • Error Tracking: Use services like Sentry or Bugsnag to automatically capture and report exceptions in both the frontend and backend API route.
  • Metrics: Monitor the performance (latency, error rate) of the /api/send-sms API route using Vercel Analytics or other monitoring tools. Track the number of successful and failed SMS attempts.
  • Health Checks: Implement a basic health check endpoint (e.g., /api/health) if needed for infrastructure monitoring.
  • Sinch Dashboard: Regularly check the Sinch dashboard for message logs, delivery statuses, and billing information.
<!-- GAP: Missing code examples for implementing monitoring hooks (Type: Substantive) --> <!-- DEPTH: Monitoring section lacks information about setting up alerts and dashboards (Priority: Medium) -->

11. Troubleshooting and Caveats

  • Error: 401 Unauthorized (from Sinch)

    • Cause: Incorrect SINCH_API_TOKEN or SINCH_SERVICE_PLAN_ID. The token might be expired or revoked. Using Basic Auth instead of Bearer Token.
    • Solution: Double-check the SINCH_API_TOKEN and SINCH_SERVICE_PLAN_ID in .env.local against the Sinch Dashboard (SMS > APIs). Ensure you are using Authorization: Bearer YOUR_TOKEN. Restart your development server after changing .env.local.
  • Error: 403 Forbidden (from Sinch)

    • Cause: The specific SERVICE_PLAN_ID might not be authorized to send SMS, or the SINCH_NUMBER (sender ID) is not valid or not associated correctly with the service plan. Trying to send to a country not enabled on your account.
    • Solution: Verify the SERVICE_PLAN_ID and SINCH_NUMBER association in the Sinch Dashboard. Check account settings for geographic restrictions.
  • Error: 400 Bad Request (from Sinch or your API Route)

    • Cause (from API Route): Input validation failed (invalid phone format, empty message). Check the details field in the JSON error response on the frontend.
    • Cause (from Sinch): Malformed request payload sent to Sinch (e.g., missing to, from, or body, invalid recipient number format after passing initial validation). Check server logs for the sinchError details returned by Sinch.
    • Solution: Correct the input data. Check the payload construction in pages/api/send-sms.js against the Sinch API documentation for the /batches endpoint.
  • Error: 500 Internal Server Error (from your API Route)

    • Cause: Missing environment variables, network error calling Sinch, unexpected exception in API route code, Sinch API itself returning a 5xx error.
    • Solution: Check server-side logs (console.error output in your terminal during development, or your logging service in production) for the specific error message. Verify .env.local variables and Sinch service status.
  • SMS Sent (200 OK) but Not Received:

    • Cause: Invalid recipient number (but valid format), recipient phone off/out of service, carrier filtering (spam filters), destination country restrictions, issues with the SINCH_NUMBER sender ID reputation.
    • Solution: Verify the recipient number is correct and active. Send a test message to your own phone. Check the Sinch Dashboard logs for detailed delivery status (if delivery reports are enabled). Contact Sinch support if issues persist.
  • Region Mismatch:

    • Cause: SINCH_REGION_URL in .env.local does not match the region where your SERVICE_PLAN_ID and API_TOKEN were generated.
    • Solution: Ensure the region URL (e.g., https://us.sms.api.sinch.com, https://eu.sms.api.sinch.com) matches your account's region.
  • Development Server Restart: Remember to restart your Next.js development server (npm run dev or yarn dev) after making changes to .env.local for them to take effect.

<!-- EXPAND: Could benefit from a decision tree flowchart for troubleshooting (Type: Enhancement) --> <!-- DEPTH: Troubleshooting lacks information about Sinch support contact methods (Priority: Low) -->

12. Deployment and CI/CD

Deploying this application is straightforward, especially using platforms like Vercel or Netlify.

Using Vercel (Recommended):

  1. Push to Git: Ensure your project (including the .gitignore file but excluding .env.local) is pushed to a Git repository (GitHub, GitLab, Bitbucket).
<!-- GAP: Missing complete deployment steps and production environment setup (Type: Critical) --> <!-- DEPTH: Deployment section is incomplete - file ends abruptly (Priority: Critical) --> <!-- GAP: Missing information about CI/CD pipeline setup (Type: Substantive) --> <!-- GAP: Missing information about environment-specific configurations (dev/staging/prod) (Type: Substantive) --> <!-- EXPAND: Could benefit from deployment checklist and post-deployment testing guide (Type: Enhancement) -->

Frequently Asked Questions

How to send SMS with Next.js?

Use Next.js API routes to securely handle server-side communication with the Sinch SMS REST API. Create a form on your frontend to collect recipient and message details. The API route fetches data from the form and sends it to Sinch, keeping your API keys hidden from the client-side.

What is the Sinch SMS REST API?

The Sinch SMS REST API is a service that allows you to send SMS messages programmatically. This guide uses the `/batches` endpoint for sending outbound messages from a Next.js application.

Why use Zod in a Next.js SMS app?

Zod provides robust input validation, ensuring data integrity and preventing invalid requests from reaching the Sinch API, thus enhancing security and preventing errors. This guide uses it to validate phone numbers (E.164 format) and message content within the API route.

When to use environment variables in Next.js?

Always use environment variables for sensitive data like API keys and secrets to protect them from being exposed in your code repository or the browser. This guide stores Sinch credentials in `.env.local`, which is only accessible server-side.

Can I send international SMS with this Next.js app?

Yes, but ensure your Sinch account and numbers are enabled for international sending. Use the E.164 phone number format (+countrycodeNUMBER) enforced by the provided Zod schema. International costs may vary, so check Sinch's pricing.

How to set up Sinch API credentials in Next.js?

Create a `.env.local` file in your project root and add `SINCH_SERVICE_PLAN_ID`, `SINCH_API_TOKEN`, `SINCH_NUMBER` (your Sinch phone number), and `SINCH_REGION_URL`. Get these values from your Sinch Dashboard under SMS > APIs, and ensure your number is linked to your service plan ID.

What is the purpose of the /batches endpoint?

The `/batches` endpoint is the standard Sinch REST API endpoint for sending one or more SMS messages. It's the most versatile and commonly used way to trigger messages programmatically, as demonstrated in this guide.

How does Next.js handle CSRF protection with the Sinch API?

Next.js has built-in CSRF protection mechanisms that enhance security for form submissions and other interactions. Since this example uses API route calls initiated from the same origin, it generally benefits from these default protections, reducing vulnerabilities.

What to do if Sinch returns a 401 Unauthorized error?

Double-check your `SINCH_API_TOKEN` and `SINCH_SERVICE_PLAN_ID` in your `.env.local` file against your Sinch Dashboard. Ensure they are correct and haven't been revoked. Also, verify you're using Bearer Token authentication (`Authorization: Bearer YOUR_TOKEN`) and restart the development server.

How to handle long SMS messages with Sinch and Next.js?

The Sinch API handles long messages (exceeding standard SMS length limits) by splitting them into multiple parts (concatenated SMS). The provided code allows up to 1600 characters, reflecting this. Be aware of potential extra costs per segment, particularly with international sending.

Why is input validation important when sending SMS?

Validating input, especially phone numbers, ensures the message is sent to the intended recipient and prevents unexpected errors or abuse of the Sinch API. The Zod library, demonstrated in this guide, prevents bad data from reaching the API and provides clear error messages.

How to troubleshoot Sinch API integration in Next.js?

Check your server-side logs for detailed error messages from both your API route and the Sinch API response. Verify environment variables, phone number formatting (E.164), and ensure the correct `SINCH_REGION_URL` is used. Refer to the troubleshooting section for common error codes and solutions.

What are best practices for logging when integrating Sinch with Next.js?

Implement structured logging (JSON format) in your API route using a library like Pino or Winston. Send these logs to a centralized logging service like Datadog, Logtail, or Axiom for analysis and alerting, particularly in a production environment.

How to improve performance for high-volume SMS sending with Sinch?

For high throughput, use a message queue (like BullMQ) to handle asynchronous sending. Offload the Sinch API call to a background worker, allowing the API route to respond quickly without blocking. Monitor Sinch's rate limits and API concurrency to optimize performance.

How to deploy a Next.js app with Sinch SMS integration?

Platforms like Vercel are well-suited. Push your project to a Git repository (excluding `.env.local`), connect it to Vercel, and set your environment variables in the Vercel dashboard to securely manage your Sinch API credentials.