code examples
code examples
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:
- A simple Next.js application with:
- A frontend form to input recipient phone numbers and message text.
- A backend API route (
/api/send-sms) that receives data from the form. - Server-side logic within the API route to securely call the Sinch SMS REST API (
/batchesendpoint) and send messages. - Clear feedback to users indicating success or failure.
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.
System Architecture:
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:2pxPrerequisites:
- 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_IDandAPI_TOKEN(found in your Sinch Customer Dashboard under SMS > APIs) - Basic familiarity with JavaScript, React, and terminal/command line usage.
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.
-
Create a Next.js App: Open your terminal and run the following command. Replace
sinch-sms-appwith your desired project name.bashnpx create-next-app@latest sinch-sms-appYou'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?YesWould you like to use Tailwind CSS?No (Keep it simple for this example)Would you like to usesrc/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)
-
Navigate into Project Directory:
bashcd sinch-sms-app -
Install Validation Library (Recommended): Use Zod to validate incoming request data in your API route.
bashnpm install zodWhy 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)
-
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.localin 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_TOKENto reveal your token. Copy both securely. - Your assigned
SINCH_NUMBERcan often be found by clicking on theSERVICE_PLAN_IDlink 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_URLfor your account.
-
* **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.
2. Implementing Core Functionality (API Route)
Create the server-side API route that handles requests from your frontend and interacts with the Sinch API.
-
Create the API Route File: Create a new file at
pages/api/send-sms.js. -
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 }); } }
Code Explanation:
-
Import Zod: Import the
zodlibrary for validation. -
Define Schema:
SendSmsSchemadefines 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.,+14151231234for a US number).
Source: ITU-T E.164 Recommendation – The International Public Telecommunication Numbering Plan
- E.164 Format: The regex
- Method Check: The
handlerfunction checks if the incoming request method isPOST. If not, it returns a 405 Method Not Allowed error. - Input Validation: Use
SendSmsSchema.parse(req.body)inside atry...catchblock. If validation fails, the code logs specific errors and returns a 400 Bad Request response to the client with details. - 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. - Construct Request: Build the full Sinch API endpoint URL. The code explicitly mentions why it uses the
/batchesendpoint. Construct the JSONpayloadrequired by the/batchesendpoint, includingfrom(using thesinchNumbervariable retrieved from environment variables),to(as an array), andbody. - Make API Call: Use the built-in
fetchAPI to make aPOSTrequest to thesinchEndpoint.- Set
Content-Type: application/json. Authorization: Bearer ${apiToken}provides the authentication token. Sinch SMS API uses Bearer token authentication with theAPI_TOKEN.- Convert the
payloadto a JSON string in the requestbody.
- Set
- 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.
- Check
- Catch Fetch Errors: A final
try...catchblock handles potential network errors during thefetchcall itself, returning a 500 error.
3. Building a Simple Frontend
Create a basic form on the homepage to trigger your API route.
-
Modify the Homepage: Open
pages/index.jsand 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> ); }
Code Explanation:
- State Variables: Use
useStateto manage the recipient number, message text, loading state, status messages, and detailed error information. handleSubmitFunction:- Prevents the default browser form submission.
- Sets
isLoadingto true to disable the button and provide feedback. - Clears previous status/error messages.
- Uses
fetchto send aPOSTrequest to your/api/send-smsendpoint. - Includes the
recipientandmessagestate values in the JSON request body. - Crucially, it checks
response.okafter parsing the JSON response (data) to handle both successful responses and structured error responses from your API route. - Updates the
statusanderrorDetailsstate based on the response. - Clears the form fields on successful submission.
- Catches potential network errors.
- Sets
isLoadingback tofalsein thefinallyblock.
- Form Elements: Standard HTML form elements (
inputfor phone,textareafor message) bound to the React state variables. Includes basic HTML5requiredvalidation. The submit button is disabled whileisLoadingis true. - Status Display: Conditionally renders a
divto show success or error messages based on thestatusstate. It includes a<pre>tag to display formatted JSON error details if available. - Styling: Uses CSS Modules by importing
styles from '../styles/Home.module.css'and applying classes likestyles.container. It assumes the defaultHome.module.cssfile generated bycreate-next-appcontains basic layout styles. Additionally, it includes inline<style jsx>for specific form and status message styling for demonstration purposes. Modifystyles/Home.module.cssor choose a different styling approach (like Tailwind, if selected during setup).
4. Integrating with Sinch (Recap)
This section serves as a checklist and recap for the Sinch-specific integration points:
- Credentials (
.env.local): EnsureSINCH_SERVICE_PLAN_ID,SINCH_API_TOKEN,SINCH_NUMBER, andSINCH_REGION_URLare correctly set in your.env.localfile. - Dashboard Navigation:
- Go to Sinch Customer Dashboard.
- SMS -> APIs: Find
SERVICE_PLAN_IDandAPI_TOKEN. - SMS -> APIs -> Click
SERVICE_PLAN_ID(or Numbers section): Find/confirm your assignedSINCH_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 usesAuthorization: Bearer YOUR_API_TOKENin thefetchrequest 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"": ""..."" }. Thefromfield uses the value stored in theSINCH_NUMBERenvironment variable.
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
fetchcall, returning a 500 error. - Logging: Uses
console.logandconsole.errorwithin 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.
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_TOKENin.env.localand 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_IDfrom.env.localand restart -> Expect a 500 error (Server configuration error).
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.
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.localand accessed only server-side. Never commit.env.localto 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
POSTmethod, rejecting others. - Rate Limiting (Consideration): For production, implement rate limiting on the
/api/send-smsendpoint 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-flexibleor@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.
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
- 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_NUMBERwith an Alphanumeric Sender ID (e.g., "MyCompany") in thefromfield of the payload. Update.env.localaccordingly if using this.
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.
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-smsAPI 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.
11. Troubleshooting and Caveats
-
Error: 401 Unauthorized (from Sinch)
- Cause: Incorrect
SINCH_API_TOKENorSINCH_SERVICE_PLAN_ID. The token might be expired or revoked. Using Basic Auth instead of Bearer Token. - Solution: Double-check the
SINCH_API_TOKENandSINCH_SERVICE_PLAN_IDin.env.localagainst the Sinch Dashboard (SMS > APIs). Ensure you are usingAuthorization: Bearer YOUR_TOKEN. Restart your development server after changing.env.local.
- Cause: Incorrect
-
Error: 403 Forbidden (from Sinch)
- Cause: The specific
SERVICE_PLAN_IDmight not be authorized to send SMS, or theSINCH_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_IDandSINCH_NUMBERassociation in the Sinch Dashboard. Check account settings for geographic restrictions.
- Cause: The specific
-
Error: 400 Bad Request (from Sinch or your API Route)
- Cause (from API Route): Input validation failed (invalid phone format, empty message). Check the
detailsfield in the JSON error response on the frontend. - Cause (from Sinch): Malformed request payload sent to Sinch (e.g., missing
to,from, orbody, invalid recipient number format after passing initial validation). Check server logs for thesinchErrordetails returned by Sinch. - Solution: Correct the input data. Check the payload construction in
pages/api/send-sms.jsagainst the Sinch API documentation for the/batchesendpoint.
- Cause (from API Route): Input validation failed (invalid phone format, empty message). Check the
-
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.erroroutput in your terminal during development, or your logging service in production) for the specific error message. Verify.env.localvariables 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_NUMBERsender 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.
- Cause: Invalid recipient number (but valid format), recipient phone off/out of service, carrier filtering (spam filters), destination country restrictions, issues with the
-
Region Mismatch:
- Cause:
SINCH_REGION_URLin.env.localdoes not match the region where yourSERVICE_PLAN_IDandAPI_TOKENwere 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.
- Cause:
-
Development Server Restart: Remember to restart your Next.js development server (
npm run devoryarn dev) after making changes to.env.localfor them to take effect.
12. Deployment and CI/CD
Deploying this application is straightforward, especially using platforms like Vercel or Netlify.
Using Vercel (Recommended):
- Push to Git: Ensure your project (including the
.gitignorefile but excluding.env.local) is pushed to a Git repository (GitHub, GitLab, Bitbucket).
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.