code examples

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

Send SMS Messages from RedwoodJS Using Node.js and Sinch API

Complete guide to integrating Sinch SMS API into RedwoodJS v8.8.1 applications using Node.js and TypeScript. Learn OAuth2 and API Token authentication, GraphQL mutations, error handling, and production deployment.

Send SMS messages from RedwoodJS using Node.js and Sinch

This guide provides a step-by-step walkthrough for integrating the Sinch SMS API into a RedwoodJS application to send SMS messages using their Node.js SDK. You'll build a simple API endpoint and service within RedwoodJS that handles sending messages via Sinch.

Project Goals:

  • Create a RedwoodJS backend service to encapsulate SMS sending logic.
  • Utilize the official Sinch Node.js SDK (@sinch/sdk-core) for reliable interaction with the Sinch SMS API.
  • Securely manage Sinch API credentials using environment variables.
  • Implement a GraphQL mutation to trigger sending SMS messages.
  • (Optional) Build a basic frontend component to interact with the mutation.
  • Include basic error handling and logging.

Technologies Used:

  • RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. Provides structure for API (Node.js), database (Prisma), and frontend (React). Current version: v8.8.1 (as of January 2025). Requires Node.js >=20.x and Yarn >=1.22.21.
  • Node.js: The runtime environment for the RedwoodJS API side. Required: Node.js v20 LTS (Maintenance LTS through October 2026) or v22 LTS "Jod" (Active LTS through October 2025, Maintenance LTS through April 2027). Node.js v20 is currently in Maintenance mode receiving critical security updates only. Node.js v22 is recommended for new projects as it remains in Active LTS throughout 2025.
  • Sinch SMS API: A service for sending and receiving SMS messages globally.
  • Sinch Node.js SDK (@sinch/sdk-core): Simplifies interactions with Sinch APIs. Current version: v1.2.1 (as of January 2025). Supports OAuth2 authentication (US and EU regions) and API Token authentication (BR, AU, CA regions, or optional for US/EU). Requires Node.js v18+ (v18 supported until May 2025), with Node.js v20 LTS or v22 LTS recommended.
  • GraphQL: Used by RedwoodJS for API communication between frontend and backend.
  • Prisma: RedwoodJS's default ORM (not strictly needed for sending, but part of the stack). Current version: Prisma ORM v6.16.0 (as of January 2025). The Rust-free architecture and ESM-first prisma-client generator are Generally Available. Prisma v7 is planned for June 2025 with Query Compiler Preview and web context support via WebAssembly.
  • TypeScript: For type safety and improved developer experience.
  • Yarn: Package manager used by RedwoodJS. RedwoodJS requires Yarn >=1.22.21. RedwoodJS v8+ supports modern Yarn versions (Yarn 4 Berry) via Corepack shipped with Node.js v18+. Yarn 1 Classic entered maintenance mode in January 2020 and receives only critical security fixes; migration to Yarn 4 Berry is recommended for new projects for improved performance (15s fresh install vs 25s in Classic) and better workspace support.

System Architecture:

+-----------------+ +--------------------+ +-----------------+ +---------------+ | RedwoodJS | ---->| RedwoodJS API | ---->| Sinch Node.js SDK | ---->| Sinch SMS API | | Frontend (React)| | (GraphQL Mutation) | | (@sinch/sdk-core)| | (Backend) | | (Optional UI) | | (Node.js Service) | +-----------------+ +---------------+ +-----------------+ +--------------------+ | | | (Request to send SMS)| (Calls Sinch SDK) +----------------------+

Prerequisites:

  • Node.js v20 LTS (Maintenance mode through October 2026) or v22 LTS "Jod" (Active LTS through October 2025, recommended for new projects in 2025). Note: RedwoodJS v8.8.1 requires Node.js >=20.x. If running Node.js v21.0.0 or higher, your project may be incompatible with some deploy targets such as AWS Lambdas.
  • Yarn v1.22.21 or higher (Classic), or Yarn 4 Berry (recommended for new projects via Corepack in Node.js v18+)
  • RedwoodJS CLI installed (yarn global add redwoodjs/cli) or use yarn create redwood-app directly
  • A Sinch account with access to the SMS API.
  • A provisioned phone number (Sender ID) in your Sinch account.
  • Sinch credentials: For OAuth2 authentication (US and EU regions): Project ID, Access Key ID, and Access Key Secret. For API Token authentication (BR, AU, CA regions, or optional for US/EU): Service Plan ID and API Token.

1. Setting up the RedwoodJS project

First, create a new RedwoodJS application if you don't already have one. We'll use TypeScript for this guide.

bash
# Create a new RedwoodJS project named 'redwood-sinch-sms'
yarn create redwood-app redwood-sinch-sms --ts

# Navigate into the project directory
cd redwood-sinch-sms

This command scaffolds a new RedwoodJS project with distinct api and web workspaces.

Install Sinch SDK:

The Sinch Node.js SDK is required on the API side to communicate with their service.

bash
# Install the Sinch SDK into the API workspace
yarn workspace api add @sinch/sdk-core

Configure Environment Variables:

Sensitive credentials like API keys should never be hardcoded. RedwoodJS uses .env files for environment variables. Create a .env file in the root of your project.

bash
# Create the .env file in the project root
touch .env

Add your Sinch credentials and sender number to the .env file. You will obtain these from your Sinch Customer Dashboard.

dotenv
# .env

# Sinch API Credentials - Choose ONE authentication method based on your region:

# METHOD 1: OAuth2 Authentication (US and EU regions only)
# Obtain from Sinch Dashboard -> Access Keys
# Ensure the key has SMS permissions
SINCH_PROJECT_ID="YOUR_PROJECT_ID"
SINCH_KEY_ID="YOUR_ACCESS_KEY_ID"
SINCH_KEY_SECRET="YOUR_ACCESS_KEY_SECRET"

# METHOD 2: API Token Authentication (BR, AU, CA regions, or optional for US/EU)
# Obtain from Sinch Dashboard -> SMS -> API Tokens
# SINCH_SERVICE_PLAN_ID="YOUR_SERVICE_PLAN_ID"
# SINCH_API_TOKEN="YOUR_API_TOKEN"

# Sinch Sender Number (Obtain from Sinch Dashboard -> Numbers -> Your Active Numbers)
# Use E.164 format (e.g., +12035551212)
SINCH_SENDER_NUMBER="+1xxxxxxxxxx"

# Optional: Sinch API Region (Defaults usually work, but specify if needed e.g., 'eu1', 'us1')
# Check Sinch SDK/API docs for valid region identifiers if needed.
# SINCH_REGION="us1"
  • OAuth2 Authentication (US and EU regions):
    • SINCH_PROJECT_ID: Found on your Sinch Dashboard overview or Access Keys page.
    • SINCH_KEY_ID / SINCH_KEY_SECRET: Generate an Access Key specifically for your application under the "Access Keys" section of the Sinch Dashboard. Important: The Key Secret is only shown once upon creation – store it securely. Ensure the key has permissions for the SMS product.
  • API Token Authentication (BR, AU, CA regions, or optional for US/EU):
    • SINCH_SERVICE_PLAN_ID: Your Service Plan ID from the Sinch Dashboard.
    • SINCH_API_TOKEN: API Token generated in the Sinch Dashboard under SMS settings.
  • SINCH_SENDER_NUMBER: A virtual phone number you have acquired or verified within your Sinch account, formatted in E.164.
  • SINCH_REGION: (Optional) If your account is provisioned in a specific region, you might need to set this. Check the Sinch documentation for valid region strings.

.env.defaults (Optional but Recommended):

It's good practice to have a .env.defaults file to show required variables without their values.

bash
# Create .env.defaults
touch .env.defaults
dotenv
# .env.defaults
# Default environment variables (do not commit actual secrets here)

# OAuth2 Authentication (US and EU regions)
SINCH_PROJECT_ID=""
SINCH_KEY_ID=""
SINCH_KEY_SECRET=""

# API Token Authentication (BR, AU, CA regions, or optional for US/EU)
# SINCH_SERVICE_PLAN_ID=""
# SINCH_API_TOKEN=""

SINCH_SENDER_NUMBER=""
# SINCH_REGION="" # Optional default region

Ensure your .gitignore file (which RedwoodJS creates automatically) includes .env to prevent committing secrets.


2. Implementing Core Functionality (API Service)

RedwoodJS uses services on the API side to encapsulate business logic. Let's create a service to handle SMS operations.

Best Practice: Singleton Client Instance

To avoid re-initializing the Sinch client on every request, which can impact performance under load, it's better to create a single instance. Create a new file api/src/lib/sinch.ts:

typescript
// api/src/lib/sinch.ts
import { SinchClient } from '@sinch/sdk-core';
import { logger } from 'src/lib/logger';

const projectId = process.env.SINCH_PROJECT_ID;
const keyId = process.env.SINCH_KEY_ID;
const keySecret = process.env.SINCH_KEY_SECRET;
const region = process.env.SINCH_REGION; // Optional region

if (!projectId || !keyId || !keySecret) {
  // Log error during initialization, but avoid throwing here
  // to allow the app to start. The service using it should handle the error.
  logger.error('Missing Sinch credentials in environment variables. Sinch client will not be functional.');
}

// Initialize the client once when the module loads
// Handle potential missing credentials gracefully
export const sinchClient = projectId && keyId && keySecret
  ? new SinchClient({
      projectId,
      keyId,
      keySecret,
      ...(region && { region }), // Conditionally add region if provided
    })
  : null; // Set to null if credentials are missing

// Helper function to get the client instance or throw if not initialized
export const getSinchClient = () => {
  if (!sinchClient) {
    throw new Error(
      'Sinch client failed to initialize. Check API credentials in environment variables.'
    );
  }
  return sinchClient;
};

Now, generate the sms service:

bash
# Generate an 'sms' service
yarn rw g service sms --ts

This creates:

  • api/src/services/sms/sms.ts: Where our sending logic will reside.
  • api/src/services/sms/sms.test.ts: For writing unit tests.

Edit api/src/services/sms/sms.ts to use the shared client:

typescript
// api/src/services/sms/sms.ts

import type { SendSmsInput } from 'types/graphql' // We will define this type later
import { getSinchClient } from 'src/lib/sinch' // Import our shared client getter
import { logger } from 'src/lib/logger' // Redwood's built-in logger

interface SendSmsArgs {
  input: SendSmsInput
}

export const sendSms = async ({ input }: SendSmsArgs) => {
  const { to, body } = input
  const sinchClient = getSinchClient() // Get the shared client instance (throws if not initialized)

  const senderNumber = process.env.SINCH_SENDER_NUMBER
  if (!senderNumber) {
    logger.error('Missing Sinch sender number in environment variables.')
    throw new Error('Sinch sender number is not configured.')
  }

  // Basic validation (Add more robust validation as needed)
  if (!to || !body) {
    throw new Error('Recipient number (to) and message body are required.')
  }
  // Basic E.164 check - consider a library like 'libphonenumber-js' for robust validation
  const e164Regex = /^\+[1-9]\d{1,14}$/;
  if (!e164Regex.test(to)) {
     // For production-ready code, throwing an error on invalid format is safer.
     // Sending with an invalid format will likely fail at the API level anyway.
     logger.error(`Invalid recipient phone number format provided: ${to}. Must be E.164.`);
     throw new Error('Invalid recipient phone number format. Use E.164 (e.g., +12035551212).');
     // Original code logged a warning and proceeded:
     // logger.warn(`Invalid recipient phone number format: ${to}. Attempting to send anyway.`);
  }

  logger.info(`Attempting to send SMS via Sinch to: ${to}`)

  try {
    const response = await sinchClient.sms.batches.send({
      sendSMSRequestBody: {
        to: [to], // Sinch expects an array of recipients
        from: senderNumber,
        body: body,
        // Optional parameters can be added here, e.g.:
        // delivery_report: 'summary', // Options: 'none', 'summary', 'full'
        // client_reference: 'your-internal-ref-123',
      },
    })

    logger.info(
      `Sinch SMS sent successfully. Batch ID: ${response.id}, To: ${to}`
    )

    // The response object contains details about the batch submission.
    // It does NOT confirm delivery, only acceptance by Sinch.
    // You might want to store the batch ID for later status checks or reconciliation.
    return {
      success: true,
      message: `SMS submitted successfully to ${to}.`,
      batchId: response.id,
      // You can return more fields from the response if needed
      // For example: response.delivery_report, response.cancelled
    }
  } catch (error) {
    logger.error({ error }, 'Failed to send SMS via Sinch') // Log the full error object

    // Try to provide a more specific error message if possible
    let errorMessage = 'Failed to send SMS due to an unexpected error.'
    // Check if it's a Sinch API error (structure might vary based on SDK version)
    if (error.response?.data) {
      logger.error({ sinchError: error.response.data }, 'Sinch API Error Details')
      // Attempt to extract a meaningful message from the Sinch error response
      const sinchErrorDetails = error.response.data.error?.text || error.response.data.message || JSON.stringify(error.response.data);
      errorMessage = `Sinch API Error: ${sinchErrorDetails}`;
    } else if (error instanceof Error) {
      errorMessage = `Failed to send SMS: ${error.message}`
    }

    // Important: Rethrow or handle appropriately.
    // Depending on your GraphQL setup, throwing here might be desired.
    // Alternatively, return a structured error response.
    // throw new Error(errorMessage); // Option 1: Throw
    return { // Option 2: Return error structure
        success: false,
        message: errorMessage,
        batchId: null,
    }
  }
}

Explanation:

  1. Import Dependencies: Import getSinchClient from our new lib file and Redwood's logger.
  2. getSinchClient Usage: Call this function to retrieve the shared, pre-initialized SinchClient instance. It throws an error if the client couldn't be initialized (e.g., missing credentials), preventing the function from proceeding incorrectly.
  3. sendSms Function:
    • Takes an input object containing to (recipient number) and body (message text).
    • Retrieves the SINCH_SENDER_NUMBER from environment variables.
    • Performs validation on inputs (presence check, E.164 format). Throws an error on invalid E.164 format. Consider using a dedicated library like libphonenumber-js for robust phone number validation in production.
    • Uses logger.info to record the attempt.
    • Calls sinchClient.sms.batches.send() inside a try...catch block.
      • The to parameter must be an array.
      • from is your configured Sinch sender number.
      • body is the message content.
    • Success: Logs success, includes the batchId returned by Sinch (useful for tracking), and returns a success object.
    • Error: Logs the error using logger.error. It attempts to extract more details from error.response.data if available (common with API errors). Returns a structured error object. Choose whether to throw or return the error structure based on how you want GraphQL to handle failures.

3. Building the API Layer (GraphQL Mutation)

RedwoodJS uses GraphQL SDL files to define the API schema. We need a mutation to trigger our sendSms service function.

Create the SDL file:

bash
# Create the GraphQL schema definition file for SMS
touch api/src/graphql/sms.sdl.ts

Edit api/src/graphql/sms.sdl.ts:

typescript
// api/src/graphql/sms.sdl.ts

export const schema = gql`
  # Input type for sending an SMS
  input SendSmsInput {
    to: String!      # Recipient phone number in E.164 format (e.g., +12035551212)
    body: String!    # Message content
  }

  # Response type for the sendSms mutation
  type SendSmsResponse {
    success: Boolean!
    message: String!
    batchId: String # The batch ID returned by Sinch upon successful submission
  }

  # Define the mutation
  type Mutation {
    # Sends an SMS message via Sinch
    sendSms(input: SendSmsInput!): SendSmsResponse! @requireAuth
    # @requireAuth directive ensures only authenticated users can call this.
    # Remove or change roles (e.g., @requireAuth(roles: "admin")) as needed.
    # If authentication isn't set up, remove @requireAuth for initial testing,
    # but **strongly** recommend adding authentication for production.
  }
`

Explanation:

  1. SendSmsInput: Defines the required input fields for the mutation: to (recipient) and body (message). Both are non-nullable strings.
  2. SendSmsResponse: Defines the structure of the data returned by the mutation: success (boolean), message (string), and an optional batchId.
  3. Mutation.sendSms: Defines the actual mutation named sendSms.
    • It accepts one argument: input of type SendSmsInput!.
    • It returns a non-nullable SendSmsResponse!.
    • @requireAuth: This RedwoodJS directive restricts access to logged-in users. If you haven't set up authentication (yarn rw setup auth ...), remove this directive for testing. Add appropriate authentication and authorization before deploying to production.

RedwoodJS automatically maps the sendSms mutation defined in the SDL to the sendSms function exported from api/src/services/sms/sms.ts. No extra resolver code is needed for this simple mapping.


4. Integrating with Sinch (Recap)

This section recaps the key integration points already covered:

  • Account Setup: Ensure you have a Sinch account, access to the SMS API, and have purchased or verified a Sender Number.
  • API Credentials: Generate an Access Key (ID and Secret) with SMS permissions from the Sinch Dashboard -> Access Keys.
  • Environment Variables: Store your SINCH_PROJECT_ID, SINCH_KEY_ID, SINCH_KEY_SECRET, and SINCH_SENDER_NUMBER securely in your .env file. Do not commit this file. Use your deployment provider's environment variable management for production.
  • SDK Installation: The @sinch/sdk-core package was installed in the API workspace (yarn workspace api add @sinch/sdk-core).
  • Client Initialization: The SinchClient is initialized centrally in api/src/lib/sinch.ts using environment variables and reused across service calls.

Dashboard Navigation Summary:

  1. Project ID: Log in to the Sinch Customer Dashboard. Your Project ID is often visible on the main dashboard or under account settings.
  2. Access Keys (Key ID, Key Secret): Navigate to ""Access Keys"" in the left-hand menu. Click ""Create Key"". Give it a name (e.g., redwood-app-sms-key), ensure it has permissions for the ""SMS"" product, and click Create. Copy the Key ID and Key Secret immediately.
  3. Sender Number: Navigate to ""Numbers"" -> ""Your Virtual Numbers"". Purchase a number suitable for sending SMS if you don't have one. Copy the number in E.164 format (e.g., +12035551212).

5. Error Handling and Logging

Our service implementation includes robust error handling and logging:

  • Credential/Config Checks: The shared client initialization (api/src/lib/sinch.ts) checks for credentials, and the getSinchClient function throws an error if the client is not available. The service also checks for the sender number.
  • Input Validation: The service includes checks for required fields (to, body) and enforces the E.164 format for the to field, throwing errors on failure. Enhance this with more specific validation logic (e.g., max message length).
  • try...catch Block: The core sinchClient.sms.batches.send call is wrapped in a try...catch block.
  • Logging: RedwoodJS's built-in Pino logger (logger) is used:
    • logger.info logs successful attempts and outcomes.
    • logger.error logs failures, including the error object and specific Sinch API error details if available. Credential loading issues are also logged.
  • Structured Error Response: The catch block currently returns a structured object ({ success: false, message: ..., batchId: null }) instead of throwing. This allows the GraphQL endpoint to always return a SendSmsResponse structure, which can be easier for the frontend to handle consistently. If you prefer GraphQL to report the error directly, replace the return in the catch block with throw new Error(errorMessage);.

Retry Mechanisms:

For transient network issues or temporary Sinch API problems, implementing a retry strategy can improve reliability. This is not included in the basic example but can be added using libraries like async-retry.

bash
# Optional: Add retry library
yarn workspace api add async-retry
yarn workspace api add -D @types/async-retry
typescript
// Example incorporating async-retry in sms.service.ts (conceptual)
import retry from 'async-retry';
import { getSinchClient } from 'src/lib/sinch'; // Adjust import if needed
// ... other imports
import type { SendSmsInput } from 'types/graphql';
import { logger } from 'src/lib/logger';

interface SendSmsArgs {
  input: SendSmsInput;
}

export const sendSms = async ({ input }: SendSmsArgs) => {
  const { to, body } = input;
  const sinchClient = getSinchClient(); // Get shared client
  const senderNumber = process.env.SINCH_SENDER_NUMBER;

  // ... validation logic ...
  if (!senderNumber) {
    logger.error('Missing Sinch sender number in environment variables.');
    throw new Error('Sinch sender number is not configured.');
  }
  if (!to || !body) {
    throw new Error('Recipient number (to) and message body are required.');
  }
  const e164Regex = /^\+[1-9]\d{1,14}$/;
  if (!e164Regex.test(to)) {
     logger.error(`Invalid recipient phone number format provided: ${to}. Must be E.164.`);
     throw new Error('Invalid recipient phone number format. Use E.164 (e.g., +12035551212).');
  }

  logger.info(`Attempting to send SMS via Sinch to: ${to} with retries`);

  try {
    const response = await retry(
      async (bail, attemptNumber) => {
        // bail is a function to stop retrying for non-recoverable errors
        try {
          logger.info(`SMS send attempt ${attemptNumber} for ${to}`);
          const result = await sinchClient.sms.batches.send({
              sendSMSRequestBody: {
                  to: [to],
                  from: senderNumber,
                  body: body,
              },
          });
          return result; // Success, return the result
        } catch (error) {
          // Example: Don't retry on specific client errors like invalid credentials (401/403)
          // or bad requests (400) that indicate a permanent issue with the input.
          const status = error.response?.status;
          if (status === 401 || status === 403 || status === 400) {
            logger.error(`Non-retryable Sinch API error (Status: ${status}). Bailing out.`);
            bail(error); // Stop retrying
            // Note: bail throws the error passed to it, so no return needed here.
          }
          // For other errors (e.g., 5xx, network errors), throw to trigger retry
          logger.warn(`SMS send attempt ${attemptNumber} failed for ${to}, retrying... Error: ${error.message}`);
          throw error; // Throw error to signal retry
        }
      },
      {
        retries: 3, // Number of retries (total attempts = retries + 1)
        factor: 2, // Exponential backoff factor
        minTimeout: 1000, // Initial delay ms
        maxTimeout: 10000, // Maximum delay ms
        onRetry: (error, attempt) => {
           logger.warn(`Retrying SMS send (Attempt ${attempt}): ${error.message}`);
        }
      }
    );

    // ... handle successful response after retries ...
     logger.info(`Sinch SMS sent successfully after retries. Batch ID: ${response.id}, To: ${to}`);
     return {
         success: true,
         message: `SMS submitted successfully to ${to}.`,
         batchId: response.id
     };

  } catch (error) {
    // ... handle final error after all retries failed ...
    logger.error({ error }, 'Failed to send SMS via Sinch after multiple retries');
     let errorMessage = 'Failed to send SMS after multiple retries.';
     if (error.response?.data) {
         const sinchErrorDetails = error.response.data.error?.text || error.response.data.message || JSON.stringify(error.response.data);
         errorMessage = `Sinch API Error: ${sinchErrorDetails}`;
     } else if (error instanceof Error) {
         errorMessage = `Failed to send SMS after retries: ${error.message}`;
     }
    return {
        success: false,
        message: errorMessage,
        batchId: null
    };
  }
};

Remember to adjust the retry logic based on the types of errors you want to retry and Sinch's specific error codes.


6. Database Schema and Data Layer

Sending a basic SMS doesn't inherently require database interaction. However, in a real application, you might want to log SMS messages sent, track their status, or associate them with users.

Optional: Logging SMS Messages

If you want to log sent messages to your database:

  1. Define Schema: Add an SmsLog model to your api/db/schema.prisma file.

    prisma
    // api/db/schema.prisma
    
    model SmsLog {
      id        Int      @id @default(autoincrement())
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
    
      recipient   String
      sender      String
      body        String
      sinchBatchId String?  @unique // Store the batch ID from Sinch
      status      String   @default(""SUBMITTED"") // e.g., SUBMITTED, FAILED_SUBMISSION, FAILED_DELIVERY, DELIVERED (requires webhooks)
      notes       String?  // Store error messages or other info
      // Optional: Link to a User model
      // userId    Int?
      // user      User?    @relation(fields: [userId], references: [id])
    }
  2. Run Migration: Apply the schema change to your database.

    bash
    yarn rw prisma migrate dev --name add_sms_log
  3. Update Service: Modify the sendSms service to create an SmsLog entry.

    typescript
    // api/src/services/sms/sms.ts
    import { db } from 'src/lib/db' // Import Redwood's Prisma client
    import { getSinchClient } from 'src/lib/sinch'
    import { logger } from 'src/lib/logger'
    import type { SendSmsInput } from 'types/graphql'
    
    interface SendSmsArgs {
      input: SendSmsInput
    }
    
    export const sendSms = async ({ input }: SendSmsArgs) => {
      const { to, body } = input;
      const sinchClient = getSinchClient();
      const senderNumber = process.env.SINCH_SENDER_NUMBER;
    
      if (!senderNumber) { /* ... handle missing sender number ... */ throw new Error('Sinch sender number is not configured.'); }
      if (!to || !body) { /* ... handle missing input ... */ throw new Error('Recipient number (to) and message body are required.'); }
      if (!/^\+[1-9]\d{1,14}$/.test(to)) { /* ... handle invalid format ... */ throw new Error('Invalid recipient phone number format. Use E.164 (e.g., +12035551212).'); }
    
      let batchId: string | null = null;
      let status: string = 'FAILED_SUBMISSION'; // Default status if submission fails
      let notes: string | null = null;
    
      try {
        const response = await sinchClient.sms.batches.send({
          sendSMSRequestBody: {
            to: [to],
            from: senderNumber,
            body: body,
          },
        });
        batchId = response.id;
        status = 'SUBMITTED'; // Successfully submitted to Sinch
        logger.info(`Sinch SMS submitted successfully. Batch ID: ${batchId}, To: ${to}`);
    
        // Create log entry on successful submission
        await db.smsLog.create({
          data: {
            recipient: to,
            sender: senderNumber,
            body: body,
            sinchBatchId: batchId,
            status: status, // SUBMITTED
          },
        });
    
        return { success: true, message: `SMS submitted successfully to ${to}.`, batchId };
    
      } catch (error) {
        logger.error({ error }, 'Failed to send SMS via Sinch');
        let errorMessage = 'Failed to send SMS due to an unexpected error.';
        if (error.response?.data) {
          const sinchErrorDetails = error.response.data.error?.text || error.response.data.message || JSON.stringify(error.response.data);
          errorMessage = `Sinch API Error: ${sinchErrorDetails}`;
        } else if (error instanceof Error) {
          errorMessage = `Failed to send SMS: ${error.message}`;
        }
        notes = errorMessage; // Store error message in notes
    
         // Log failed submission attempt to DB
        try {
           await db.smsLog.create({
             data: {
               recipient: to,
               sender: senderNumber, // Might be null if config fails early
               body: body,
               sinchBatchId: batchId, // Will be null if submission failed
               status: 'FAILED_SUBMISSION',
               notes: notes,
             },
           });
        } catch (logDbError) {
            logger.error({ logDbError }, ""Failed to write failure SMS log to database""); // Avoid crashing if DB logging fails
        }
    
        return { success: false, message: errorMessage, batchId: null };
      }
    }

Note: Tracking actual delivery status (DELIVERED, FAILED_DELIVERY after submission) requires setting up Sinch webhooks to receive Delivery Reports, which is covered conceptually in Section 10.


7. Security Features

  • Authentication/Authorization: As mentioned, the @requireAuth directive on the GraphQL mutation is crucial. Ensure you have a proper authentication system (yarn rw setup auth ...) and potentially role-based access control (@requireAuth(roles: ["admin"])) depending on who should be allowed to send SMS.
  • Input Validation:
    • GraphQL type validation provides basic checks.
    • The service includes checks for required fields (to, body) and E.164 format.
    • Implement stricter validation for the to field using a library like libphonenumber-js (yarn workspace api add libphonenumber-js) to ensure it's a valid phone number type (e.g., mobile).
    • Validate message body length against SMS segment limits (typically 160 GSM-7 characters or 70 UCS-2 characters per segment) or Sinch API limits if applicable. Truncate or reject messages that are too long.
  • Secret Management: Never commit .env files containing secrets. Use environment variables provided securely by your deployment host (e.g., Vercel, Netlify, AWS Secrets Manager).
  • Rate Limiting: Protect your endpoint from abuse. RedwoodJS doesn't have built-in rate limiting per-endpoint, but you can integrate solutions:
    • Use GraphQL middleware like graphql-shield or graphql-rate-limit-directive.
    • Implement logic within your service (e.g., check timestamps in the SmsLog table for a specific user or recipient within a time window).
    • Use infrastructure-level rate limiting (e.g., Cloudflare, API Gateway, Nginx).
  • Preventing Injection: Using the Sinch SDK generally prevents command injection risks associated with crafting raw HTTP requests. Ensure any dynamic content included in the SMS body is properly sanitized if it originates from untrusted user input (though typically SMS bodies are less susceptible than HTML).

8. Handling Special Cases

  • Phone Number Formatting: Always use and expect the E.164 format (+ followed by country code and number, e.g., +14155552671, +442071838750). Validate inputs strictly using a library like libphonenumber-js.
  • Character Encoding & Concatenation: Standard SMS messages have character limits. Longer messages are split (concatenated) into multiple segments, which may incur higher costs.
    • GSM-7 encoding (standard characters) allows ~160 chars/segment.
    • UCS-2 encoding (for Unicode characters like emojis, non-Latin scripts) allows ~70 chars/segment.
    • The Sinch API handles concatenation automatically, but be mindful of message length for cost and user experience. You can estimate segments or check the num_parts field in Sinch delivery reports (requires webhook setup) to see how many segments were used. Consider warning users or truncating long messages.
  • Internationalization: Sinch supports global sending, but regulations vary significantly by country (e.g., sender ID restrictions, required opt-in/opt-out language, content restrictions, time-of-day sending windows). Ensure compliance for target countries. The E.164 format handles country codes inherently.
  • Sender ID: In some countries, you might use an Alphanumeric Sender ID (e.g., ""MyCompany"") instead of a phone number. This requires registration with Sinch and is subject to country-specific rules (often cannot receive replies). Configure this in your Sinch dashboard and potentially adjust the from parameter logic if needed (e.g., use a different environment variable or logic based on the recipient's country).

9. Performance Optimizations

  • Client Instantiation: We've implemented the best practice of initializing the SinchClient once in api/src/lib/sinch.ts and reusing the instance (singleton pattern), avoiding overhead on each request.
  • Asynchronous Operations: Sending SMS is an I/O-bound operation. RedwoodJS and Node.js handle this asynchronously (async/await), preventing the main thread from blocking. Ensure your service function remains async.
  • Payload Size: Keep the SMS body concise to minimize segment count and cost.
  • Batching (Sinch API Feature): The Sinch batches.send endpoint supports sending the same message to multiple recipients in one API call by providing an array in the to field (to: ["+1...", "+44...", ...]). This is more efficient than multiple individual API calls for bulk broadcasts. The current example sends to one recipient per call but can be adapted. Note that sending different messages to different recipients in one call requires a more complex batch structure (refer to Sinch API docs). Error handling for partial failures within a batch also needs careful consideration.
  • Database Operations: If logging to a database (SmsLog), ensure database operations are efficient (e.g., indexing sinchBatchId). Avoid complex queries within the critical path of sending the SMS if possible.

10. Monitoring, Observability, and Analytics

  • Logging: RedwoodJS's logger provides the foundation. Ensure logs are captured, aggregated, and searchable in your deployment environment (e.g., Vercel Log Drains, Netlify Functions logs, Datadog, Logtail). Log key information like batchId, recipient (potentially masked/anonymized depending on privacy rules), success/failure status, and error details.
  • Error Tracking: Integrate an error tracking service (e.g., Sentry, Bugsnag) with RedwoodJS (yarn rw setup apm <provider>) to capture and alert on errors from your API side, including those from the SMS service or client initialization.
  • Sinch Dashboard: The Sinch Customer Dashboard is essential for monitoring:
    • SMS Logs / Traffic Log: View details of sent messages, their status (requires delivery reports for final status), cost, error codes, etc. Filter by batchId, recipient, sender, date range.
    • Usage Reports / Statistics: Track overall volume, spending, and delivery rates over time.
  • Health Checks: Create a simple GraphQL query or HTTP endpoint in your RedwoodJS app that performs a basic check (e.g., confirms environment variables are loaded, maybe checks connectivity if Sinch offers a status endpoint). Monitor this endpoint with uptime services (e.g., UptimeRobot, Pingdom).
  • Delivery Reports (Webhooks): For robust status tracking (e.g., DELIVERED, FAILED after submission), this is crucial.
    • Configure in Sinch: Set up Delivery Reports (often called DLRs) in your Sinch API settings, providing a URL endpoint where Sinch can POST status updates.
    • Implement Webhook Handler in RedwoodJS: Create a RedwoodJS function (e.g., yarn rw g function sinchDlrWebhook --ts) that acts as the webhook receiver endpoint. This function needs to parse the incoming POST request from Sinch, validate it (e.g., using a shared secret or signature verification if offered by Sinch), and update the corresponding SmsLog entry in your database with the final delivery status (DELIVERED, FAILED, etc.).

Frequently Asked Questions (FAQ)

How do I send SMS messages from RedwoodJS with Sinch API?

Install the Sinch Node.js SDK (@sinch/sdk-core v1.2.1) in your RedwoodJS API workspace using yarn workspace api add @sinch/sdk-core. Create a service with yarn rw g service sms --ts, configure Sinch credentials in .env (OAuth2 for US/EU regions or API Token for BR/AU/CA regions), initialize a singleton SinchClient in api/src/lib/sinch.ts, and implement a GraphQL mutation that calls sinchClient.sms.batches.send() with the recipient number (E.164 format), sender number, and message body.

Which Node.js version should I use for RedwoodJS v8.8.1 with Sinch?

Use Node.js v22 LTS "Jod" (Active LTS through October 2025, Maintenance LTS through April 2027) for new RedwoodJS projects in 2025. RedwoodJS v8.8.1 requires Node.js >=20.x minimum. Node.js v20 LTS remains supported (Maintenance mode through October 2026) but receives only critical security updates. Avoid Node.js v21.0.0+ as it may cause incompatibility with deploy targets like AWS Lambdas.

What are the differences between OAuth2 and API Token authentication in Sinch SDK?

OAuth2 authentication (available in US and EU regions) uses Project ID, Access Key ID, and Access Key Secret credentials obtained from Sinch Dashboard → Access Keys. API Token authentication (required for BR, AU, CA regions, optional for US/EU) uses Service Plan ID and API Token from SMS settings. The @sinch/sdk-core v1.2.1 SDK supports both methods – configure the appropriate credentials in your .env file based on your account region.

How do I handle errors when sending SMS via Sinch in RedwoodJS?

Wrap the sinchClient.sms.batches.send() call in a try...catch block. Log errors using RedwoodJS's Pino logger (logger.error), extract Sinch API error details from error.response.data, and return a structured response object with success: false and an error message. Implement retry logic using the async-retry library for transient network failures, but bail on non-retryable errors like 401 (unauthorized), 403 (forbidden), or 400 (bad request) status codes.

What is E.164 phone number format and why is it required?

E.164 is the international phone number format required by Sinch API: a plus sign (+) followed by the country code and number without spaces or special characters (e.g., +12035551212 for US, +442071838750 for UK). This format ensures global interoperability. Validate recipient numbers using the regex /^\+[1-9]\d{1,14}$/ or use the libphonenumber-js library (yarn workspace api add libphonenumber-js) for robust validation including mobile number verification.

How do I set up authentication for the RedwoodJS SMS GraphQL mutation?

Add the @requireAuth directive to your sendSms mutation in api/src/graphql/sms.sdl.ts. First, set up authentication in RedwoodJS using yarn rw setup auth <provider> (e.g., Auth0, Supabase, Netlify Identity). For role-based access control, use @requireAuth(roles: ["admin"]) to restrict SMS sending to specific user roles. Remove @requireAuth only for initial testing, but always implement authentication before production deployment to prevent abuse.

Should I use Yarn 1 Classic or Yarn 4 Berry for RedwoodJS projects?

RedwoodJS v8.8.1 requires Yarn >=1.22.21 and supports both Yarn 1 Classic and Yarn 4 Berry. Yarn 4 Berry is recommended for new projects – it offers 60% faster fresh installs (15s vs 25s), better workspace support for monorepos, and Plug'n'Play mode to reduce disk space. RedwoodJS projects using Node.js v18+ can use Yarn 4 via Corepack automatically. Yarn 1 Classic entered maintenance mode in January 2020 and receives only critical security fixes.

How do I track SMS delivery status in RedwoodJS with Sinch?

The batches.send() response returns a batchId indicating successful submission to Sinch, not delivery confirmation. For final delivery status (DELIVERED, FAILED), set up Sinch Delivery Reports (DLRs) in your dashboard with a webhook URL. Create a RedwoodJS function (yarn rw g function sinchDlrWebhook --ts) to receive POST requests from Sinch with status updates. Parse the webhook payload, validate it using a shared secret, and update your SmsLog database records with the final status.

What are SMS message segment limits and how do they affect costs?

Standard SMS messages use GSM-7 encoding (~160 characters per segment) or UCS-2 encoding (~70 characters per segment for Unicode characters like emojis). Longer messages are automatically split (concatenated) into multiple segments by Sinch, with each segment billed separately. The Sinch API handles concatenation transparently. Check the num_parts field in delivery reports to see segment count. Validate message length in your service and consider warning users or truncating messages exceeding single-segment limits to control costs.

How do I implement rate limiting for the RedwoodJS SMS endpoint?

RedwoodJS doesn't include built-in per-endpoint rate limiting. Implement protection using: (1) GraphQL middleware like graphql-shield or graphql-rate-limit-directive to limit mutation calls per user, (2) Service-level logic querying the SmsLog table to count messages sent by a user/recipient within a time window and reject requests exceeding thresholds, or (3) Infrastructure-level rate limiting via Cloudflare, AWS API Gateway, or Nginx reverse proxy rules. Combine with @requireAuth authentication to track per-user limits effectively.

What Prisma ORM version should I use with RedwoodJS v8.8.1?

RedwoodJS v8.8.1 ships with Prisma ORM v6.16.0 (as of January 2025), which includes the Generally Available Rust-free architecture and ESM-first prisma-client generator. This version offers better performance in serverless/edge environments and eliminates binary compatibility issues. Prisma v6 includes AI safety features that recognize execution by AI coding assistants like Claude Code, native D1 migration support, and custom database driver support for PlanetScale and Neon. Prisma v7 (planned June 2025) will make Rust-free architecture the default and add WebAssembly CLI support for web-based editors.

How do I deploy a RedwoodJS SMS application to production?

Deploy the RedwoodJS API side to serverless platforms like Vercel (recommended for RedwoodJS), Netlify Functions, or AWS Lambda. Configure environment variables (SINCH_PROJECT_ID, SINCH_KEY_ID, SINCH_KEY_SECRET, SINCH_SENDER_NUMBER) securely using your platform's environment variable management (never commit .env files). Set up a PostgreSQL database using providers like Supabase, Neon, or AWS RDS. Run yarn rw deploy <provider> or follow platform-specific deployment guides. Configure Sinch webhook URLs for delivery reports pointing to your production domain. Monitor logs using Vercel Log Drains, Datadog, or Logtail, and integrate error tracking with Sentry or Bugsnag.

Frequently Asked Questions

How to send SMS messages with RedwoodJS?

You can send SMS messages within a RedwoodJS application by integrating the Sinch SMS API and using their Node.js SDK. This involves creating a RedwoodJS backend service and a GraphQL mutation to trigger the message sending process via Sinch.

What is the Sinch SMS API used for in RedwoodJS?

The Sinch SMS API allows your RedwoodJS application to send and receive SMS messages globally. It's integrated using the Sinch Node.js SDK (@sinch/sdk-core) for simplified interaction.

Why use environment variables for Sinch API keys?

Storing sensitive credentials like Sinch API keys in environment variables ensures they are not hardcoded into your application, enhancing security. RedwoodJS uses .env files to manage these variables.

When should I create a singleton Sinch client instance?

Creating a single Sinch client instance is a performance best practice. Do this during initialization to avoid re-initializing the client with every request, especially under heavy load.

What is the role of a RedwoodJS service in sending SMS?

RedwoodJS services encapsulate backend logic. In this case, an SMS service handles the interaction with the Sinch SDK, including sending messages and managing responses.

How to integrate Sinch SDK into RedwoodJS project?

Install the Sinch Node.js SDK (`@sinch/sdk-core`) in your RedwoodJS API side using `yarn workspace api add @sinch/sdk-core`. Then, configure your environment variables with your Sinch credentials.

How to handle errors when sending SMS with Sinch?

Implement a try...catch block around the Sinch SDK's send method and use RedwoodJS's logger to record successes and failures. The example code returns structured error objects, letting GraphQL handle responses consistently. Consider retry mechanisms for transient issues using libraries like `async-retry`.

How to structure Sinch SMS error handling in RedwoodJS?

Use RedwoodJS's logger within a try...catch block around the Sinch API call. Log failures with `logger.error` and provide helpful error messages in the returned object or by throwing errors, depending on your GraphQL error handling strategy.

How to store sent SMS messages in a database?

While not strictly required, you can create an SmsLog model in your Prisma schema and update your service to create a log entry for each sent message, including status and other relevant details.

Can I track SMS delivery status with Sinch and RedwoodJS?

Yes, by configuring Sinch Delivery Reports (webhooks) in your Sinch API settings and creating a corresponding RedwoodJS function to receive and process the delivery status updates. This allows you to update your database with the final delivery status (e.g., DELIVERED, FAILED).

How to secure Sinch SMS integration in RedwoodJS?

Use environment variables for API keys, implement authentication/authorization with the `@requireAuth` directive, validate input data thoroughly, and consider rate limiting to protect your endpoint from abuse.

What is the recommended format for phone numbers when sending SMS?

Always use the E.164 format for phone numbers (e.g., +14155552671). Validate input using a library like `libphonenumber-js` for accuracy and security.

Why are retries important for Sinch SMS integration and how to implement them?

Network or temporary Sinch API issues can cause SMS sending to fail. Retries improve reliability. Use the `async-retry` library to implement retries in your Redwood service. Ensure to handle different error scenarios appropriately within the retry logic and avoid retrying on non-recoverable errors such as invalid credentials.

How can I improve performance of Sinch SMS sending in RedwoodJS?

Initialize the SinchClient once and reuse the instance. Also, use asynchronous operations, keep the SMS body concise, and leverage the Sinch API's batch sending feature for multiple recipients. If logging to a database, optimize database queries and indexing.

What are ways to monitor Sinch SMS integration and track performance?

Use RedwoodJS's logging, integrate error tracking services like Sentry, monitor the Sinch Dashboard for SMS logs and usage reports, and set up health checks for your RedwoodJS API. Implementing Sinch Delivery Reports (webhooks) allows detailed tracking of delivery statuses.