code examples
code examples
RedwoodJS SMS Marketing: Sinch Batch API Integration Tutorial (2025)
Learn how to integrate Sinch Batch SMS API with RedwoodJS for marketing campaigns. Complete tutorial with Node.js, GraphQL, Prisma ORM, code examples, and bulk SMS sending implementation.
Integrate Sinch Batch SMS API with RedwoodJS for Marketing Campaigns
Learn how to build a complete SMS marketing system by integrating the Sinch Batch SMS API with your RedwoodJS application. This step-by-step tutorial covers campaign creation, contact management, bulk SMS sending, and database setup using GraphQL and Prisma ORM.
This RedwoodJS SMS integration tutorial provides programmatic control over SMS marketing campaigns with automated message creation, targeted bulk sending, and centralized management. We'll use the Sinch Batch SMS API endpoint (/xms/v1/{SERVICE_PLAN_ID}/batches) for sending marketing messages to multiple recipients. For advanced features like audience segmentation or detailed analytics, consult the official Sinch SMS API documentation.
Technologies Used:
- RedwoodJS: Full-stack, serverless-first web framework based on React, GraphQL, and Prisma. Provides rapid development structure, built-in API layer, and CLI tools.
- Node.js: Runtime environment for the RedwoodJS API side, executing backend logic and Sinch API interactions.
- Prisma: Next-generation ORM for Node.js and TypeScript, handling database modeling and access.
- Sinch Batch SMS API: Third-party SMS service for marketing message delivery.
- GraphQL: Communication layer between frontend (web) and backend (API).
System Architecture:
+-------------------+ +-----------------------+ +-----------------------+ +---------------------+
| RedwoodJS Web UI | ---> | RedwoodJS GraphQL API | ---> | Sinch API Client | ---> | Sinch Batch SMS API |
| (React Components)| | (api side) | | (api/src/lib/sinchClient) | | (REST) |
+-------------------+ +----------+------------+ +----------+------------+ +----------+----------+
| ^ | ^ |
| User Interaction | | GraphQL Query/Mutation | Sinch API Calls | SMS Sending
v | v | v
+-------------------+ +----------+------------+ +----------+------------+
| RedwoodJS Router | | RedwoodJS Services | | Prisma ORM |
| (Routes.tsx) | | (api/src/services) | | (schema.prisma) |
+-------------------+ +----------+------------+ +----------+------------+
| ^
| Database Operations |
v |
+----------+------------+
| Database (e.g., PostgreSQL) |
+-----------------------+What You'll Build:
- RedwoodJS application configured for Sinch Batch SMS API
- Database models for
CampaignandContactdata - RedwoodJS services managing campaigns and contacts
- Node.js module encapsulating Sinch API interactions
- Secure Sinch API credential handling
- Error handling and logging for the integration
- Deployment and verification instructions
Note: This guide focuses on backend implementation. UI components (pages, cells) for campaign management are outlined but require additional implementation for production use. Consider adding monitoring, analytics tracking, and comprehensive error recovery for production deployments.
Prerequisites:
- Node.js v18+ and Yarn v1 installed
- Terminal or command prompt access
- Sinch account with SMS API access (includes Service Plan ID, API Token, and registered Sender ID)
- Basic understanding of RedwoodJS, React, GraphQL, Prisma, and REST APIs
- Code editor (e.g., VS Code)
Important: SMS messaging incurs costs. Review Sinch pricing and set up spending alerts before sending production messages. This guide assumes a new RedwoodJS project – adapt steps for existing projects.
1. RedwoodJS Project Setup for SMS Integration
Create a new RedwoodJS application and configure environment variables for Sinch API authentication.
-
Create RedwoodJS App: Open your terminal and create the project named
redwood-sinch-campaigns:bashyarn create redwood-app redwood-sinch-campaigns --typescriptThe
--typescriptflag initializes the project with TypeScript for type safety. -
Navigate to Project Directory:
bashcd redwood-sinch-campaigns -
Environment Variables Setup: Create a
.envfile in the project root for your Sinch credentials:bashtouch .envAdd the following variables, replacing placeholders with your actual Sinch credentials:
dotenv# .env # Sinch API Credentials # Obtain from your Sinch Dashboard -> APIs -> Your SMS API -> API Credentials SINCH_SERVICE_PLAN_ID="YOUR_SERVICE_PLAN_ID" SINCH_API_TOKEN="YOUR_API_TOKEN" # Sinch Sender ID (Registered Number/Short Code/Alphanumeric) # This MUST be a sender ID approved for your account in the Sinch dashboard SINCH_SENDER_ID="YOUR_REGISTERED_SENDER_ID" # Sinch API Endpoint (Confirm the correct region endpoint if needed) SINCH_API_BASE_URL="https://us.sms.api.sinch.com" # Or eu.sms.api.sinch.com, etc. # Database URL (Example for local SQLite, replace for PostgreSQL/MySQL) DATABASE_URL="file:./dev.db"Variable Explanations:
SINCH_SERVICE_PLAN_ID: Unique identifier for your Sinch service planSINCH_API_TOKEN: Secret authentication token (treat like a password)SINCH_SENDER_ID: Registered phone number, short code, or alphanumeric sender ID approved by SinchSINCH_API_BASE_URL: Base URL for Sinch REST API (adjust for your region: us, eu, etc.)DATABASE_URL: Database connection string (adjust provider inschema.prisma)
Security Best Practices:
- Verify
.envis in your.gitignorefile (RedwoodJS adds this by default) - Never commit credentials to version control
- Use environment-specific variables for staging and production
- Rotate API tokens regularly
- Enable Sinch API IP restrictions if available
-
Install Dependencies: Install
node-fetchfor HTTP requests to the Sinch API. While Node.js includes built-in fetch, explicitly addingnode-fetchensures compatibility across versions:bashyarn workspace api add node-fetch@^2 # Use v2 for CommonJS compatibility with Redwood api side yarn workspace api add -D @types/node-fetch@^2yarn workspace api add: Installs packages for the API side onlynode-fetch@^2: Version 2 for CommonJS compatibility@types/node-fetch@^2: TypeScript typings
-
Project Structure Overview:
Backend (
api/):api/db/schema.prisma: Database schema definitionapi/src/functions/: Serverless function handlers (GraphQL endpoint)api/src/graphql/: GraphQL schema definitions (*.sdl.ts)api/src/services/: Business logic implementationsapi/src/lib/: Utility functions and third-party clients
Frontend (
web/):web/src/pages/: Page componentsweb/src/components/: Reusable UI components (including Cells)web/src/layouts/: Layout componentsweb/src/Routes.tsx: Frontend routing
Configuration:
.env: Environment variables (Git-ignored)redwood.toml: Project configuration
2. Database Schema and Service Implementation
Design Prisma database models for contacts and campaigns, then implement RedwoodJS services for business logic.
-
Define Database Schema: Open
api/db/schema.prismaand add models forContactandCampaign:prisma// api/db/schema.prisma datasource db { provider = "sqlite" // Or "postgresql", "mysql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model Contact { id Int @id @default(autoincrement()) phoneNumber String @unique // Assuming phone number is the unique identifier (E.164 format recommended) firstName String? lastName String? // Add status for opt-outs, etc. // status String @default("ACTIVE") // e.g., ACTIVE, OPTED_OUT createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Add other relevant fields like email, tags, etc. } model Campaign { id Int @id @default(autoincrement()) name String messageBody String // Basic status tracking status String @default("DRAFT") // e.g., DRAFT, SENDING, SENT, FAILED // Store the Batch ID returned by Sinch API after successful sending sinchBatchId String? @unique // Renamed for clarity based on API used scheduledAt DateTime? // Optional: For scheduling campaigns sentAt DateTime? // Record when sending was initiated/completed createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relation to contacts (can be complex - many-to-many, or simplified for now) // For simplicity here, we assume campaigns are sent to all contacts or a segment defined elsewhere. // A many-to-many relation might look like: // contacts Contact[] @relation("CampaignContacts") @@index([status]) // Add index for faster status queries } // If using many-to-many for contacts per campaign: // model _CampaignContacts { // A Int // B Int // @@id([A, B]) // campaign Campaign @relation("CampaignContacts", fields: [A], references: [id]) // contact Contact @relation("CampaignContacts", fields: [B], references: [id]) // @@index([A]) // @@index([B]) // }Schema Notes:
- Store phone numbers in E.164 format (e.g.,
+15551234567) sinchBatchIdstores the ID returned by Sinch for tracking- Index on
Campaign.statusimproves query performance - Consider adding many-to-many relations between contacts and campaigns for advanced segmentation
- GDPR Compliance: Add opt-in status, consent timestamp, and unsubscribe tracking fields for production use
- Store phone numbers in E.164 format (e.g.,
-
Apply Database Migrations: Create and apply a database migration based on the schema changes:
bashyarn rw prisma migrate dev --name create_contacts_campaignsThis command creates a new SQL migration file in
api/db/migrations/, applies the migration to your development database, and generates/updates the Prisma Client. -
Generate Services: Generate boilerplate code for services that handle business logic:
bashyarn rw g service contact yarn rw g service campaignThis creates
api/src/services/contacts/contacts.ts,api/src/services/campaigns/campaigns.ts, and corresponding test/scenario files. -
Implement Service Logic (Basic CRUD): Open the generated service files and add basic CRUD operations.
api/src/services/contacts/contacts.ts:typescriptimport type { QueryResolvers, MutationResolvers } from 'types/graphql' import { db } from 'src/lib/db' import { requireAuth } from 'src/lib/auth' // Assuming auth will be set up export const contacts: QueryResolvers['contacts'] = () => { requireAuth() return db.contact.findMany() } export const contact: QueryResolvers['contact'] = ({ id }) => { requireAuth() return db.contact.findUnique({ where: { id }, }) } interface CreateContactInput { phoneNumber: string // Add validation for E.164 format here or in GraphQL layer firstName?: string lastName?: string } export const createContact: MutationResolvers['createContact'] = ({ input }: { input: CreateContactInput }) => { requireAuth() // TODO: Add validation for phoneNumber format (E.164) return db.contact.create({ data: input, }) } // Add updateContact and deleteContact if neededProduction Recommendations: Implement E.164 phone number validation using a library like
libphonenumber-js. Add duplicate detection and merge logic for contacts with existing phone numbers.api/src/services/campaigns/campaigns.ts:typescriptimport type { QueryResolvers, MutationResolvers } from 'types/graphql' import { db } from 'src/lib/db' import { requireAuth } from 'src/lib/auth' // Assuming auth is set up // We will import the Sinch client later // import { sendSinchSmsBatch } from 'src/lib/sinchClient' export const campaigns: QueryResolvers['campaigns'] = () => { requireAuth() return db.campaign.findMany({ orderBy: { createdAt: 'desc' } }) } export const campaign: QueryResolvers['campaign'] = ({ id }) => { requireAuth() return db.campaign.findUnique({ where: { id }, }) } interface CreateCampaignInput { name: string messageBody: string } export const createCampaign: MutationResolvers['createCampaign'] = ({ input }: { input: CreateCampaignInput }) => { requireAuth() // Just create in DB for now. Sending logic will be added later. return db.campaign.create({ data: { ...input, status: 'DRAFT' }, }) } // Placeholder for the sending action (Implementation in Section 4) export const sendCampaign = async ({ id }: { id: number }) => { requireAuth() console.log(`Placeholder: Logic to send campaign ${id} via Sinch would go here.`) // 1. Fetch campaign details from DB // 2. Fetch target contacts from DB (e.g., all active contacts) // 3. Call the Sinch API client (to be built in Section 4) // 4. Update campaign status in DB (e.g., SENDING, SENT, FAILED) // 5. Store Sinch Batch ID const tempCampaign = await db.campaign.findUnique({ where: { id } }); // Fetch for return type consistency return { success: true, message: `Campaign ${id} sending initiated (placeholder).`, campaign: tempCampaign } } // Add updateCampaign and deleteCampaign if neededProduction Recommendations: Add pagination, filtering, and sorting capabilities for the campaigns query. Implement cursor-based pagination for large datasets.
Why RedwoodJS Services? Services encapsulate business logic, making your SMS marketing code modular, testable, and reusable. They bridge the GraphQL API layer with Prisma database operations and external API integrations like Sinch. For more on RedwoodJS architecture, see the official RedwoodJS documentation.
-
Data Layer Summary:
ERD (Simplified):
text+-------------+ +--------------+ | Contact | | Campaign | |-------------| |--------------| | PK id | | PK id | | phoneNumber | | name | | firstName | | messageBody | | lastName | | status | | createdAt | | sinchBatchId?| | updatedAt | | scheduledAt? | | | | sentAt? | | | | createdAt | | | | updatedAt | +-------------+ +--------------+ | | (Many-to-Many possible but not shown)- Data Access: Handled by Prisma Client via Redwood services (
db.campaign.findMany(),db.contact.create(), etc.). Redwood'sdbobject is an instance ofPrismaClient. - Migrations: Managed by
yarn rw prisma migrate dev. For production, useyarn rw prisma migrate deploy.
- Data Access: Handled by Prisma Client via Redwood services (
-
Seed Sample Data (Optional): Use Prisma seeds to populate your development database.
Create seed file:
bashtouch api/db/seed.tsAdd seed logic:
typescript// api/db/seed.ts import { PrismaClient } from '@prisma/client' const db = new PrismaClient() async function main() { console.log('Start seeding…') // Seed Contacts await db.contact.upsert({ where: { phoneNumber: '+15551112222' }, update: {}, create: { phoneNumber: '+15551112222', firstName: 'Alice' }, // Use valid E.164 formats }) await db.contact.upsert({ where: { phoneNumber: '+15553334444' }, update: {}, create: { phoneNumber: '+15553334444', firstName: 'Bob', lastName: 'Smith' }, }) console.log('Seeded contacts.') // Seed Campaigns await db.campaign.upsert({ where: { name: 'Welcome Campaign Draft' }, // Use a unique field if possible update: {}, create: { name: 'Welcome Campaign Draft', messageBody: 'Welcome to our service!', status: 'DRAFT' } }) console.log('Seeded campaigns.') console.log('Seeding finished.') } main() .catch((e) => { console.error(e) process.exit(1) }) .finally(async () => { await db.$disconnect() })Configure the seed command in
prismasection of rootpackage.jsonif not present:json// package.json (root) "prisma": { "seed": "yarn rw exec seed" }Run seeding:
bashyarn rw prisma db seed
3. GraphQL API Layer Configuration
Define GraphQL schemas (SDL) to expose campaign and contact operations through RedwoodJS's type-safe API layer.
-
Generate GraphQL SDL: Generate SDL files based on your Prisma schema models:
bashyarn rw g sdl contact --crud yarn rw g sdl campaign --crudThe
--crudflag generates standard GraphQL types, queries, and mutations based on the model and corresponding service functions. -
Review and Customize SDL: Inspect the generated files (
api/src/graphql/contacts.sdl.tsandapi/src/graphql/campaigns.sdl.ts). Customize as needed:api/src/graphql/contacts.sdl.ts:typescriptexport const schema = gql` type Contact { id: Int! phoneNumber: String! firstName: String lastName: String createdAt: DateTime! updatedAt: DateTime! } type Query { contacts: [Contact!]! @requireAuth contact(id: Int!): Contact @requireAuth } input CreateContactInput { phoneNumber: String! # Consider adding scalar type for E.164 validation firstName: String lastName: String } input UpdateContactInput { phoneNumber: String firstName: String lastName: String } type Mutation { createContact(input: CreateContactInput!): Contact! @requireAuth # updateContact and deleteContact mutations will also be generated if using --crud # updateContact(id: Int!, input: UpdateContactInput!): Contact! @requireAuth # deleteContact(id: Int!): Contact! @requireAuth } `Enhancement Opportunity: Implement custom GraphQL scalar types for E.164 phone number validation. Add field-level documentation using GraphQL description strings.
api/src/graphql/campaigns.sdl.ts:typescriptexport const schema = gql` type Campaign { id: Int! name: String! messageBody: String! status: String! sinchBatchId: String # Renamed field scheduledAt: DateTime sentAt: DateTime createdAt: DateTime! updatedAt: DateTime! } type Query { campaigns: [Campaign!]! @requireAuth campaign(id: Int!): Campaign @requireAuth } input CreateCampaignInput { name: String! messageBody: String! } input UpdateCampaignInput { name: String messageBody: String status: String sinchBatchId: String # Renamed field scheduledAt: DateTime sentAt: DateTime } # Add a mutation for our custom send action type SendCampaignResponse { success: Boolean! message: String campaign: Campaign # Return updated campaign } type Mutation { createCampaign(input: CreateCampaignInput!): Campaign! @requireAuth # updateCampaign, deleteCampaign mutations... # updateCampaign(id: Int!, input: UpdateCampaignInput!): Campaign! @requireAuth # deleteCampaign(id: Int!): Campaign! @requireAuth sendCampaign(id: Int!): SendCampaignResponse! @requireAuth # Custom mutation } `@requireAuth: RedwoodJS directive for enforcing authentication on GraphQL operations. Configure RedwoodJS Authentication (yarn rw setup auth ...) before production deployment. During development, you can temporarily remove this directive, but always restore it for security.SendCampaignResponse&sendCampaignMutation: Custom mutation to trigger campaign sending logic in the service. -
Testing API Endpoints (GraphQL Playground):
Start the development server:
bashyarn rw devOpen your browser to
http://localhost:8910/graphql(or the port specified in the console output). Test your queries and mutations.Example Query (Get Campaigns):
graphqlquery GetCampaigns { campaigns { id name status createdAt sinchBatchId # Added field } }Example Mutation (Create Campaign):
graphqlmutation CreateNewCampaign { createCampaign(input: { name: "May Promo Blast" messageBody: "Hey {firstName}, check out our May deals! Limited time only." }) { id name messageBody status } }Note:
@requireAuthmight block requests if auth isn't configured. Remove it from the SDL temporarily for testing if needed, but ensure it's present for production.
4. Sinch Batch SMS API Integration
Implement a Node.js client to send bulk SMS messages using the Sinch API's /batches endpoint.
-
Create Sinch Client Library: Create a new file for Sinch API interaction logic:
bashmkdir -p api/src/lib/sinchClient touch api/src/lib/sinchClient/index.ts -
Implement Sinch API Client Logic: Open
api/src/lib/sinchClient/index.tsand add the code to interact with Sinch:typescript// api/src/lib/sinchClient/index.ts import fetch from 'node-fetch' // Using node-fetch v2 const SINCH_SERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID const SINCH_API_TOKEN = process.env.SINCH_API_TOKEN const SINCH_API_BASE_URL = process.env.SINCH_API_BASE_URL || 'https://us.sms.api.sinch.com' // Default fallback interface SinchAPIErrorResponse { request_id?: string; // Optional based on Sinch error format error?: { code: number; message: string; reference?: string; }; // Batch specific errors might have different formats text_code?: string; text_message?: string; } // Helper function to make authenticated requests to Sinch API async function sinchRequest<T>( endpoint: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', body?: Record<string, any> ): Promise<T> { if (!SINCH_SERVICE_PLAN_ID || !SINCH_API_TOKEN) { throw new Error('Sinch API credentials (SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN) are not configured in .env') } const url = `${SINCH_API_BASE_URL}/xms/v1/${SINCH_SERVICE_PLAN_ID}${endpoint}` const headers = { 'Authorization': `Bearer ${SINCH_API_TOKEN}`, 'Content-Type': 'application/json', 'Accept': 'application/json', } console.log(`Sinch Request: ${method} ${url}`) // Basic logging try { const response = await fetch(url, { method: method, headers: headers, body: body ? JSON.stringify(body) : undefined, }) if (!response.ok) { // Attempt to parse error details from Sinch, which can vary let errorDetails: string | SinchAPIErrorResponse = await response.text(); let errorMessage = response.statusText; try { const parsedError = JSON.parse(errorDetails) as SinchAPIErrorResponse; console.error('Sinch API Error Response:', parsedError); // Try different common error message fields errorMessage = parsedError.error?.message || parsedError.text_message || errorMessage; throw new Error(`Sinch API Error (${response.status}): ${errorMessage}. Ref: ${parsedError.error?.reference || parsedError.request_id || 'N/A'}`); } catch (parseError) { // If parsing fails, use the raw text console.error('Sinch API Error Response (non-JSON or parse error):', errorDetails); throw new Error(`Sinch API Error (${response.status}): ${response.statusText}. Response: ${errorDetails}`); } } // Handle cases where Sinch might return 204 No Content or similar success without body if (response.status === 204 || response.headers.get('content-length') === '0') { // Return an empty object or specific success indicator if needed // For batch send (POST /batches), Sinch *does* return a body on success (200 or 201) return {} as T; // Adjust as needed for other endpoints } // Assuming successful responses are JSON const data = await response.json() as T console.log(`Sinch Response (${response.status}):`, data) // Basic logging return data; } catch (error) { console.error(`Error during Sinch API request to ${endpoint}:`, error) // Re-throw a more specific error or handle appropriately // Avoid exposing raw Sinch errors directly to frontend if possible throw new Error(`Failed to communicate with Sinch API: ${error.message}`); } } // --- Specific Sinch API Functions --- interface SendSMSInput { to: string[]; // Array of phone numbers in E.164 format from: string; // Your Sinch virtual number, Short Code, or Alphanumeric Sender ID body: string; // The message content // Common optional parameters for batch SMS: delivery_report?: 'none' | 'summary' | 'full'; // default 'none' client_reference?: string; // Your custom ID for tracking parameters?: Record<string, { default?: string; [phoneNumber: string]: string }>; // For message templating/personalization // See: https://developers.sinch.com/docs/sms/api-reference/sms/tag/Batches/#tag/Batches/operation/SendSMS } interface SendSMSResponse { id: string; // The Batch ID returned by Sinch to: string[]; from: string; canceled: boolean; body: string; type: string; // e.g., "mt_batch" created_at: string; // ISO 8601 date string modified_at: string; // ISO 8601 date string delivery_report: string; // ... other fields like 'send_at', 'expire_at', 'flash_message', 'feedback_enabled' etc. } /** * Sends an SMS batch message using the Sinch API. * This function uses the standard Sinch Batch SMS endpoint (`/xms/v1/{SERVICE_PLAN_ID}/batches`). * While suitable for sending marketing messages, check Sinch documentation for any dedicated * "Marketing Campaign" APIs if you need features beyond batch sending (e.g., advanced list management). */ export async function sendSinchSmsBatch(input: SendSMSInput): Promise<SendSMSResponse> { // Input validation (basic) if (!input.to || input.to.length === 0) { throw new Error('Recipient list (`to`) cannot be empty.'); } if (!input.from) { throw new Error('Sender ID (`from`) is required.'); } if (!input.body) { throw new Error('Message body (`body`) is required.'); } // TODO: Add more robust validation (e.g., E.164 format for 'to' numbers, length checks for 'body') const endpoint = '/batches' // Endpoint for sending batches return sinchRequest<SendSMSResponse>(endpoint, 'POST', { to: input.to, from: input.from, body: input.body, delivery_report: input.delivery_report || 'summary', // Request a summary DLR client_reference: input.client_reference, // Pass through client reference if provided parameters: input.parameters, // Pass through parameters if provided }) } // Add other functions as needed, e.g., getBatchStatus(batchId), cancelBatch(batchId), etc. // Consult the Sinch SMS API documentation for the correct endpoints and payloads.Key Features:
- Reads credentials and base URL from
process.env - Includes a
sinchRequesthelper function handling authentication, headers, request/response logging, and error handling for all Sinch API calls - Provides
sendSinchSmsBatchto interact with the/batchesendpoint for sending messages to multiple recipients - Includes basic input validation and improved error parsing
- API Key Security: Credentials are read from environment variables, never hardcoded
Production Recommendations: Implement retry logic with exponential backoff, rate limiting to respect Sinch API quotas, and circuit breaker patterns for fault tolerance. Consider using libraries like
axios-retryorp-retryfor robust SMS delivery. For more on SMS API best practices, see our guide on bulk SMS broadcasting. - Reads credentials and base URL from
-
Obtaining Sinch Credentials:
Step Action 1 Log in to your Sinch Customer Dashboard 2 Navigate to SMS → APIs section 3 Find your Service Plan ID 4 Under API Credentials, generate or copy an API Token 5 Note your registered Sender ID (under Numbers or Sender IDs) – must be approved for sending 6 Copy these values into your .envfile7 Confirm the correct API Base URL for your account's region (e.g., us.sms.api.sinch.com,eu.sms.api.sinch.com)Troubleshooting Tips:
- Verify sender ID is approved in your Sinch dashboard before testing
- Check API token hasn't expired
- Confirm you're using the correct regional endpoint
- Review Sinch account quotas and spending limits
-
Connect Sinch Client to Campaign Service: Update the
sendCampaignfunction inapi/src/services/campaigns/campaigns.tsto use the Sinch client:typescript// api/src/services/campaigns/campaigns.ts import type { QueryResolvers, MutationResolvers, CampaignResolvers } from 'types/graphql' // Added CampaignResolvers import { db } from 'src/lib/db' import { sendSinchSmsBatch } from 'src/lib/sinchClient' // ... (rest of the imports and existing code like campaigns, campaign, createCampaign) ...
Note: Complete implementation of sendCampaign function with full error handling, transaction management, delivery tracking, and UI components requires additional sections (5–8) covering frontend implementation, testing strategies, production deployment, monitoring setup, and comprehensive troubleshooting guides. Implement these features before production use.
Next Steps for Production SMS Marketing
To deploy a production-ready RedwoodJS SMS marketing platform:
- Implement Complete sendCampaign Function: Add error handling, database transactions, contact fetching, and Sinch API integration
- Build Frontend UI: Create React components, pages, and cells for campaign management
- Add Testing: Write unit tests for services, integration tests for API endpoints, and end-to-end tests for critical workflows
- Configure Deployment: Set up environment variables for production, configure database migrations, and deploy to your hosting platform
- Implement Monitoring: Add logging, error tracking (e.g., Sentry), and performance monitoring
- Ensure Compliance: Implement GDPR-compliant opt-in/opt-out mechanisms, consent tracking, and data retention policies
- Add Analytics: Track campaign performance, delivery rates, and user engagement metrics
Frequently Asked Questions
How to send SMS marketing campaigns with RedwoodJS?
Integrate the Sinch Batch SMS API into your RedwoodJS application. This involves setting up environment variables for your Sinch credentials, creating database models for campaigns and contacts, and implementing RedwoodJS services to manage these entities and interact with the Sinch API. This guide provides a step-by-step tutorial for this integration process.
What is the Sinch Batch SMS API used for in RedwoodJS?
The Sinch Batch SMS API allows you to send bulk SMS messages programmatically within your RedwoodJS app, making it suitable for marketing campaigns. This guide focuses on integrating with the `/xms/v1/{SERVICE_PLAN_ID}/batches` endpoint, enabling automated campaign creation and targeted sending. Note that Sinch may offer dedicated Marketing Campaign APIs with additional features.
How to integrate Sinch API with RedwoodJS services?
You can create a dedicated Sinch API client within your `api/src/lib` directory to handle the API interaction logic. Then import and call the client functions from your RedwoodJS services, specifically within the service responsible for managing and sending campaigns.
Why use Prisma in the RedwoodJS and Sinch integration?
Prisma acts as an ORM (Object-Relational Mapper), simplifying database interactions within your RedwoodJS application. It allows you to define data models (like Contacts and Campaigns) and easily perform CRUD operations without writing raw SQL queries.
What RedwoodJS services are needed for SMS campaigns?
You primarily need services for managing campaigns and contacts. The campaign service handles creating, updating, and sending campaigns, while the contact service manages your contact list. These services interact with Prisma for database access and the Sinch client for sending SMS messages.
How to set up Sinch API credentials in RedwoodJS?
Create a `.env` file in the root of your RedwoodJS project. Add your `SINCH_SERVICE_PLAN_ID`, `SINCH_API_TOKEN`, `SINCH_SENDER_ID`, and `SINCH_API_BASE_URL` obtained from your Sinch dashboard to this file. Your `.env` file should already be in `.gitignore` for security.
What is the role of GraphQL in RedwoodJS SMS campaigns?
GraphQL serves as the communication layer between the RedwoodJS frontend (web side) and backend (api side). You define GraphQL schemas (SDL) to specify data types and queries/mutations. RedwoodJS automatically generates resolvers that map these to your service functions.
How to create a new RedwoodJS project for Sinch integration?
Use the command `yarn create redwood-app <project-name> --typescript` in your terminal. The `--typescript` flag is recommended for better type safety. Then, navigate into the project directory using `cd <project-name>`.
When should I use node-fetch in a RedwoodJS project?
While Node.js has a built-in `fetch`, using `node-fetch@^2` explicitly ensures better CommonJS compatibility, particularly with the RedwoodJS api side. Install it with `yarn workspace api add node-fetch@^2` and its types with `yarn workspace api add -D @types/node-fetch@^2`.
What database is used in this RedwoodJS Sinch example?
The example uses SQLite for simplicity, but you can configure PostgreSQL or MySQL in your `schema.prisma` file. The `DATABASE_URL` in your `.env` file controls the database connection. For local SQLite, use `DATABASE_URL="file:./dev.db"`.
How to handle Sinch API errors in RedwoodJS?
The provided `sinchRequest` helper function in the Sinch client includes basic error handling and logging. It attempts to parse error responses from Sinch to provide more detailed messages for debugging. Always avoid exposing raw Sinch errors to the frontend if possible.
Can I use a custom sender ID with the Sinch API?
Yes, you can use a registered phone number, short code, or alphanumeric sender ID approved by Sinch. Set the `SINCH_SENDER_ID` environment variable in your `.env` file. This value is then used in the `from` field of the API request.
What is the purpose of the Sinch service plan ID?
The Sinch Service Plan ID is a unique identifier that specifies the service plan you're using within the Sinch platform. It's required to make authenticated requests to the Sinch API and is included in the API endpoint URL.