Frequently Asked Questions
Navigate to your RedwoodJS api directory using your terminal, then run 'yarn workspace api add @sinch/sdk-core'. This command adds the Sinch Node.js SDK to your project's API side, allowing you to interact with the Sinch SMS API.
Integrate the Sinch SMS API into your RedwoodJS application. This involves setting up your project, configuring your database with Prisma, creating necessary services and GraphQL endpoints, and then leveraging the Sinch Node.js SDK to send messages via the API. This setup allows you to manage contacts and broadcast messages efficiently.
RedwoodJS is the core framework for building the bulk SMS application. It provides structure for the backend API using GraphQL and services, connects to a database via Prisma, and offers a frontend framework with React. This allows for a streamlined development process.
Sinch provides a reliable and powerful SMS API along with an official Node.js SDK. The API allows for efficient bulk messaging by enabling sending to multiple recipients in a single API call, simplifying the integration and improving performance.
Prisma acts as the Object-Relational Mapper (ORM) connecting your RedwoodJS application to your chosen database (PostgreSQL or SQLite). It simplifies database interactions by defining models (like Contact and Broadcast) in a schema file, allowing you to query and manage data using JavaScript.
RedwoodJS uses a .env file in the project root. Create or open this file and add your Sinch credentials like Project ID, Key ID, Key Secret, and your Sinch virtual number. Make sure to add .env to your .gitignore file to protect your secrets.
Implement a try-catch block around the sinchClient.sms.batches.send API call. Inside the catch block, update the broadcast status to 'FAILED', log detailed error messages including the Sinch API response if available, and throw an error to notify the user.
Fetching all contacts at once using db.contact.findMany() is not scalable. Implement background jobs with Redwood's exec command or a task queue like BullMQ. The job should process contacts in batches, ensuring the application doesn't timeout when sending to thousands of recipients.
Create GraphQL SDL and corresponding service functions to handle contact creation. Implement input validation, specifically for phone numbers using E.164 formatting, and use Prisma to save the contact data to the database.
Login to the Sinch Customer Dashboard. Note your Project ID, navigate to Access Keys, and generate a new key pair (Key ID and Key Secret). Save the Key Secret securely as it's only displayed once. Find your provisioned Sinch phone number under Numbers > Your Virtual Numbers.
Use the BroadcastRecipient model when you need detailed tracking of individual message statuses within a broadcast. This join table helps manage retries, provide detailed reporting, and understand specific delivery failures, especially for large-scale SMS campaigns.
In this implementation, 'SENT' signifies that the Sinch API has accepted the batch send request. It doesn't guarantee delivery to the recipient. To track actual delivery, you must configure Sinch Delivery Report Webhooks.
Yes, the current design allows retrying a 'FAILED' broadcast by calling the sendBroadcast mutation again. For robust automated retries against transient issues, consider setting up a background job queue system with exponential backoff.
Use background jobs, database batching, and select specific data fields in queries. Instead of pulling all contacts at once, which can lead to timeouts, process them in smaller groups in the background and use Prisma's 'select' feature to only retrieve necessary data.
The guide emphasizes storing API keys securely as environment variables, validating phone number format using E.164, using Redwood's @requireAuth directive for access control, and handling errors robustly to prevent information leakage. It also recommends using additional measures like rate limiting for production deployments.
Building Bulk SMS Broadcast Systems with Sinch, RedwoodJS, and Node.js
This comprehensive guide walks you through building a production-ready bulk SMS broadcasting system using the Sinch SMS API integrated with RedwoodJS. You'll implement contact management, batch messaging for thousands of recipients, GraphQL APIs, Prisma database modeling, error handling, and deployment strategies for scalable SMS marketing campaigns, alerts, and notifications.
The final application enables you to manage contact lists and send custom broadcast messages to thousands of recipients efficiently via Sinch's batch API. Use this for marketing campaigns, emergency alerts, appointment reminders, and transactional notifications.
Prerequisites: Node.js v18+, Yarn, a Sinch account with API credentials, and a provisioned Sinch virtual number. This tutorial requires familiarity with JavaScript and full-stack frameworks.
Core Technologies:
System Architecture:
By completing this guide, you'll build a RedwoodJS application with:
Note: This guide emphasizes backend API implementation with RedwoodJS Services and GraphQL. Section 11 provides a frontend structure overview.
1. RedwoodJS Project Setup and Sinch API Configuration
Initialize your RedwoodJS project and configure environment variables for Sinch.
1.1. Create RedwoodJS Project
Open your terminal and run:
Follow the prompts. Choose TypeScript if preferred (examples use JavaScript). Select PostgreSQL for production or SQLite for development.
1.2. Install Sinch SDK
Navigate to the API workspace and add the Sinch Node.js SDK:
1.3. Configure Environment Variables
Create or open the
.envfile in your project root and add your Sinch credentials:How to obtain Sinch credentials:
SINCH_FROM_NUMBERSecurity: Never commit your
.envfile to version control. Redwood adds.envto.gitignoreby default. Use your deployment provider's secure environment variable management for production.1.4. Initialize Database
Set up your database connection string in
.envand run the initial migration command:This ensures your database is ready for the schemas we'll define next.
2. Database Schema Design with Prisma for SMS Broadcasting
Define your contacts and broadcasts models in the database.
2.1. Define Prisma Schema
Open
api/db/schema.prismaand define the models:Why these models?
Contact: Stores essential recipient information. Use E.164 format forphoneNumberfor international compatibility.Broadcast: Tracks message content and overall broadcast status.BroadcastRecipient(Optional but Recommended): Join table tracking status of each individual recipient within a broadcast. Essential for retries, reporting, and debugging failures with large lists.2.2. Apply Database Migrations
Run the migration command again to apply these schema changes to your database:
This updates your database schema and generates the corresponding Prisma Client types.
3. GraphQL API Layer: SDL Schema and Service Implementation
Define the GraphQL interface and implement the backend logic in Redwood Services.
3.1. Define GraphQL Schema (SDL)
Define GraphQL types, queries, and mutations for managing contacts and broadcasts.
api/src/graphql/contacts.sdl.ts:api/src/graphql/broadcasts.sdl.ts:@requireAuth: Ensures only authenticated users can access these operations. Remove this for internal tools without authentication, but securing API endpoints is recommended.createBroadcastsaves the message content.sendBroadcasttriggers the Sinch API interaction.3.2. Implement Services
Generate the corresponding service files:
Now, implement the logic within these services.
api/src/services/contacts/contacts.ts:api/src/services/broadcasts/broadcasts.ts:SinchClientis instantiated using credentials from environment variables. (Recommendation: Move tolibfor larger projects).SINCH_FROM_NUMBER, and atry…catchblock around thesinchClient.sms.batches.sendcall. Failures update the broadcast status toFAILEDand log detailed errors.loggerfor informative messages. Logs Sinch API errors if available.sinchClient.sms.batches.send, which is designed for sending the same message to multiple recipients efficiently.Broadcaststatus (PENDING→SENDING→SENT/FAILED). TheSENTstatus indicates acceptance by Sinch, not final delivery.BroadcastRecipientCreation (Optional): Creates records linking contacts to the broadcast, storing the Sinchbatch_id.recipientCount): Dynamically calculates the recipient count for a broadcast query using the join table.findMany()without batching for large lists.4. Error Handling, Logging, and Retry Mechanisms
Error Handling Strategy:
try...catcharound the critical Sinch API callFAILED) on errorsLogging:
logger.info,logger.warn,logger.errorwithin services (Redwood configures Pino logger by default)Retry Mechanisms:
FAILEDbroadcast by calling thesendBroadcastmutation again.sendBroadcastmutation: Queue a background job instead of sending directly (see Section 7)execcommand with Faktory/Temporal) that supports automatic retries with backoffFAILED5. Security Best Practices for SMS Broadcasting
Authentication & Authorization:
dbAuthis a simple starting point:@requireAuthdirective added to the SDLs in Section 3 now enforces that only logged-in users can perform contact/broadcast operations.ADMIN,USER) using Redwood's RBAC features (@requireAuth(roles: 'ADMIN')) to restrict who can send broadcasts. See Redwood RBAC Docs.Input Validation & Sanitization:
createContactandcreateBroadcast).createContactTODO).createBroadcast.Rate Limiting:
rate-limiter-flexibleand Redis to limit requests per user or IP to your GraphQL endpoint, especially thesendBroadcastmutation.Sinch Security: Rely on secure storage of Sinch API keys (environment variables). Do not expose keys on the client-side. Validate the
SINCH_FROM_NUMBERformat.6. Handling SMS Special Cases and Compliance
Phone Number Formatting: Strictly enforce E.164 format (
+followed by country code and number, no spaces or dashes) for all phone numbers stored and sent to Sinch. Validate on input (see Section 5).Character Encoding & Message Length: Be mindful of SMS character limits. Standard GSM-7 allows 160 chars per segment. Using non-GSM characters (like emojis, some accented letters) switches to UCS-2, reducing the limit to 70 chars per segment. Long messages are split into multiple segments by carriers, potentially increasing costs. Inform users or truncate messages if necessary (validate in
createBroadcast). The Sinch API handles segmentation, but costs are per segment.Opt-Outs/Consent Management: Regulations like TCPA (US) and GDPR (EU) require managing user consent and honoring opt-out requests.
subscribedboolean field (defaulting totrue) to theContactmodel. Modify thefindManyquery insendBroadcastto filter contacts (where: { subscribed: true }). Implement a mechanism (e.g., handling replies like "STOP" via Sinch Inbound SMS webhooks) to update thesubscribedstatus tofalse.Duplicate Phone Numbers: The
Contactmodel has@uniqueonphoneNumberto prevent duplicates. Handle potential errors duringcreateContactif a number already exists (Prisma will throw an error; catch it and provide a user-friendly message).Internationalization: E.164 format handles country codes. Be aware of varying regulations and costs for sending SMS to different countries. Ensure your Sinch account is enabled for the destination countries.
7. Performance Optimization and Background Job Processing
Database Queries:
selectin Prisma queries (findMany,findUnique) to fetch only the necessary fields (as done insendBroadcastfor contacts)@@index([field])inschema.prisma) to frequently queried fields (e.g.,statusonBroadcast). The@uniqueonphoneNumberalready creates an indexBatching: The current implementation uses Sinch's batch send (
sms.batches.send), which is efficient for the API call itself. The bottleneck for large lists is often the database query (db.contact.findMany()).Background Jobs (Crucial for Large Scale):
sendBroadcastimplementation fetching all contacts at once is not scalable.sendBroadcastmutation: Mark the broadcast asQUEUEDorPENDINGand enqueue a background job, passing thebroadcastIdexec: Useyarn rw exec <script-name>with a task queue like Faktory or TemporalbroadcastIdskipandtake)SENDING,SENT,FAILED) and potentially individualBroadcastRecipientstatusesCaching: Not typically a major factor for this specific workflow unless contact lists are extremely static and queried very frequently on the frontend.
8. Monitoring, Observability, and Analytics for SMS Campaigns
Health Checks: Add a simple health check endpoint to your Redwood API (e.g., using a custom function or a simple GraphQL query) that verifies database connectivity. Monitor this endpoint using uptime monitoring services (e.g., UptimeRobot, Better Uptime).
Performance Metrics:
sendBroadcastservice function execution, especially the database query and the Sinch API call timesError Tracking:
logger.error(including Sinch API error details) are captured or shipped to your logging platformSinch Dashboard & Analytics:
Custom Dashboards (Optional): Use tools like Grafana (with Prometheus or Loki for logs/metrics) or your logging/APM provider's dashboarding features to visualize:
SENTvsFAILEDstatus in your DB)9. Troubleshooting Common Sinch Integration Issues
Common Errors:
401 Unauthorizedfrom Sinch: IncorrectSINCH_PROJECT_ID,SINCH_KEY_ID, orSINCH_KEY_SECRET. Double-check values in.envor production environment variables. Ensure the key is active in the Sinch dashboard.403 Forbiddenfrom Sinch: The API key might lack permissions for the SMS API, or theSINCH_FROM_NUMBERmight not be provisioned correctly, enabled for the destination country, or properly formatted (needs E.164). Check Sinch dashboard settings (API keys, Number configuration, Allowed Countries).400 Bad Requestfrom Sinch: Invalid phone number format in thetolist (ensure all are E.164), message too long, missing required parameters (from,to,body), or invalidSINCH_FROM_NUMBERformat. Check the error details logged from the Sinch SDK/API response.DATABASE_URLis correct and the database server is running and accessible. Check firewall rules.sendBroadcasttakes too long (usually due to fetching/processing large contact lists), implement background jobs (Section 7). This is the most common scaling issue.Sinch Platform Limitations:
429 Too Many Requests. Implement delays or use background job queues with rate limiting features if hitting limits.toarray. For larger campaigns, split recipients into multiple batches.send_atby default (configurable viaexpire_atparameter, max 3 days).Development vs Production:
yarn rw prisma migrate deploy(notmigrate dev) in production to apply migrations without prompts.Testing Considerations:
POST /xms/v1/{service_plan_id}/batches/dry_run) to validate batch requests without sending actual SMS messages. This returns the number of recipients, message parts, and encoding information.10. Production Deployment Strategies for RedwoodJS
Deploy your RedwoodJS application to a hosting provider that supports both static frontend (Web side) and serverless/Node.js backend (API side).
Recommended Platforms:
Deployment Checklist:
SINCH_PROJECT_ID,SINCH_KEY_ID,SINCH_KEY_SECRET,SINCH_FROM_NUMBER, andDATABASE_URLin your platform's environment settingsDATABASE_URL11. Frontend Implementation Overview with React and GraphQL
While this guide focuses on backend implementation, here's a brief overview of the frontend structure:
Contact Management UI (
web/src/pages/ContactsPage/ContactsPage.tsx):Cellpattern (automatically handles loading/error states)contacts) and mutations (createContact,deleteContact)Broadcast Management UI (
web/src/pages/BroadcastsPage/BroadcastsPage.tsx):sendBroadcastmutation for a selected broadcastExample Cell Pattern for Broadcasts:
For detailed frontend implementation, refer to the RedwoodJS Tutorial which covers Cells, Forms, and GraphQL integration comprehensively.
FAQ
How do I get Sinch API credentials for my RedwoodJS bulk SMS application?
Log in to your Sinch Customer Dashboard, locate your Project ID on the homepage, then navigate to Access Keys in the left menu. Generate a new access key to receive your Key ID and Key Secret (displayed only once – save immediately to your password manager). Find your SMS-enabled virtual number under Numbers → Your Virtual Numbers. Add these credentials to your RedwoodJS
.envfile asSINCH_PROJECT_ID,SINCH_KEY_ID,SINCH_KEY_SECRET, andSINCH_FROM_NUMBER(in E.164 format like+14155552671).What is the maximum batch size for Sinch SMS API in RedwoodJS?
Sinch's official
/batchesAPI endpoint supports a maximum of 1,000 recipients per batch request in thetoarray parameter. For campaigns exceeding 1,000 contacts, implement database pagination using Prisma'sskipandtakemethods to fetch contacts in chunks of 1,000. Process each chunk as a separate Sinch batch request. For production scale (10,000+ contacts), use background job queues (BullMQ with Redis) to handle batching without serverless timeout issues.How do I validate E.164 phone numbers in RedwoodJS services before sending SMS?
Install the
libphonenumber-jsvalidation library withyarn workspace api add libphonenumber-js. In your RedwoodJS service function, useparsePhoneNumber()to validate and format numbers:This ensures all phone numbers are stored in standardized E.164 format (
+[country code][number]) for international SMS delivery compatibility.What are SMS character encoding limits with Sinch batch messaging?
Sinch supports message bodies up to 2,000 characters total. However, SMS carriers automatically split messages into segments: GSM-7 encoding (standard Latin alphabet) allows 160 characters per segment, while UCS-2 encoding (Unicode characters, emojis, accented letters) reduces this to 70 characters per segment. A 320-character GSM-7 message sends as 3 segments and is billed as 3 SMS messages. Use Sinch's dry run endpoint (
POST /xms/v1/{service_plan_id}/batches/dry_run) to preview message segmentation and character encoding before sending production campaigns.How do I debug Sinch API authentication errors in RedwoodJS?
401 Unauthorizedresponses indicate credential mismatches. Verify your.envfile contains exact values from Sinch Dashboard:SINCH_PROJECT_ID,SINCH_KEY_ID, andSINCH_KEY_SECRETwith no extra whitespace or quotes. Check the access key status in your Sinch Dashboard under Access Keys – ensure it's active and has SMS API permissions enabled. The@sinch/sdk-coreautomatically handles Bearer token authentication, so verify your credentials grant access to the SMS service specifically (not just other Sinch products like Voice or Verification).How can I prevent serverless function timeouts when sending bulk SMS in RedwoodJS?
Serverless platforms (Vercel, Netlify) enforce 10–60 second execution limits per function invocation. For broadcasts exceeding a few hundred contacts:
yarn workspace api add bullmq redissendBroadcastmutation to enqueue a background job instead of sending directlyskip/take)This architecture decouples broadcast triggering from actual SMS sending, preventing timeouts while processing large contact lists. The GraphQL mutation returns immediately with "QUEUED" status while the worker handles Sinch API calls asynchronously.
Why use the BroadcastRecipient join table in the Prisma schema?
The
BroadcastRecipientmodel enables granular per-recipient tracking within each broadcast campaign. It stores individual delivery status (PENDING,SENT,FAILED), Sinch batch IDs for webhook correlation, delivery timestamps, and specific failure reasons. This is essential for:Without this join table, you only track overall broadcast status, losing visibility into which specific contacts succeeded or failed.
How do I configure Sinch delivery reports in RedwoodJS to track SMS delivery?
Set the
delivery_reportparameter when callingsinchClient.sms.batches.sendin your service:none: No delivery reports (default, fastest)summary: Single webhook callback with aggregate statisticsfull: Single webhook with array of per-recipient delivery statusesper_recipient: Individual webhook per message status change (use cautiously for large batches – generates many callbacks)Configure a webhook URL in your Sinch Dashboard (Service Plan settings) pointing to a RedwoodJS function endpoint like
/api/sinch-delivery-webhook. Create the function to parse incomingDeliveryReportpayloads and updateBroadcastRecipientrecords with final delivery status. See Sinch Delivery Reports API Reference for webhook payload schemas.How do I implement STOP keyword opt-out handling for SMS compliance in RedwoodJS?
Add a
subscribedboolean field to your PrismaContactmodel:Run
yarn rw prisma migrate devto apply the schema change. Update yoursendBroadcastservice to filter:db.contact.findMany({ where: { subscribed: true } }). Implement a separate inbound SMS webhook handler (using Sinch Inbound SMS API) to detect "STOP", "UNSUBSCRIBE", or "QUIT" keywords and execute:db.contact.update({ where: { phoneNumber }, data: { subscribed: false } }). This ensures TCPA (US), GDPR (EU), and PDPA (Singapore) compliance by honoring opt-out requests automatically.What are the best practices for testing Sinch SMS integration in RedwoodJS before production?
Follow this 6-step testing strategy:
POST /batches/dry_runendpoint withper_recipient: trueto preview message segmentation without sending@sinch/sdk-core:SINCH_PROJECT_IDvalues for dev/staging to prevent accidental production sendslogger.infooutput shows correct API parameters before enabling real sendsThis staged approach catches configuration errors, validates message formatting, and ensures proper error handling before exposing the system to customers.