Frequently Asked Questions
You can send SMS messages by creating a RedwoodJS service that uses the Sinch SMS API. This service will handle the logic for sending messages and interacting with your database to store message history. A GraphQL mutation will expose this functionality to your RedwoodJS frontend.
Ngrok creates a secure tunnel from a public URL to your local development environment. This is essential for testing webhooks locally because Sinch needs to deliver messages to a publicly accessible URL. Without ngrok, Sinch cannot reach your local server during development.
The .env file stores sensitive configuration details such as API keys and database URLs, keeping them separate from your codebase. This approach enhances security and prevents accidental exposure of credentials in version control systems like Git. The file should not be committed to your repository.
Verification is the first step after the inboundSms function is invoked. Always verify the webhook's signature to confirm its origin and integrity. This process prevents malicious actors from spoofing requests, protecting your data and application.
Yes, this guide describes setting up a Prisma schema and migrations to store message data like body, sender/receiver numbers, timestamps, and Sinch's external ID for each message. This provides a robust record of all sent and received SMS activity for your application.
Set up a Redwood Function as a webhook handler. Configure the webhook in the Sinch dashboard and point its target URL to your Redwood function's publicly accessible endpoint (using ngrok during development). The webhook will then deliver incoming messages to your application as they arrive.
Redwood Functions handle serverless logic, perfect for webhook endpoints. The inboundSms
function receives data from Sinch when an SMS is sent to your Sinch virtual number. The function then stores the message information in the database and can initiate logic based on the incoming message (like automated replies).
Sinch's Conversation API provides a unified approach for handling messages across multiple channels (SMS, chat, etc.). While the dedicated SMS API can handle outbound messages, the Conversation API's webhooks are the standard way to receive inbound SMS through Sinch.
For simple outbound-only SMS, the SMS API (used via @sinch/sdk-core
here) might be simpler. For inbound messages or scenarios requiring more advanced Conversation features (e.g., multiple channels), use the Conversation API's webhooks and associated functionalities.
Yes, ngrok creates a tunnel to your local development environment, enabling Sinch to send webhook requests to your inboundSms
function locally. Ensure both your Redwood server and ngrok are running during local tests.
Sinch automatically retries webhook deliveries based on the status code your function returns. Return 5xx status codes (e.g., 500 Internal Server Error) for errors you want Sinch to retry. Return a 2xx code (e.g., 200 OK) when successful.
The provided sendSms
service includes a try...catch
block. Enhance it for production by implementing retries for transient errors, catching Sinch-specific errors, and logging error details appropriately.
It's crucial for securing your webhook. It's used to verify the signature of incoming requests, ensuring they originate from Sinch. This prevents unauthorized actors from sending fake webhook requests.
Add a Message
model to your schema.prisma
file. This model should include fields like body
, fromNumber
, toNumber
, direction
, status
, externalId
, and timestamps. Run yarn rw prisma migrate dev
to apply the changes to your database.
Sinch Two-Way SMS with RedwoodJS: Complete Webhook Integration Guide
You'll integrate Sinch Short Message Service (SMS) capabilities into a RedwoodJS application in this step-by-step guide, enabling both outbound and inbound (two-way) messaging. You'll build a system where your application can send SMS messages via Sinch and receive incoming messages sent to your Sinch virtual number through webhooks.
Project Goals:
Core Technologies:
ngrok
(for development): A tool to expose local development servers to the internet, necessary for testing Sinch webhooks.System Architecture:
Prerequisites:
ngrok
installed globally (npm install -g ngrok
oryarn global add ngrok
) for local webhook testing.Final Outcome:
By the end of this guide, you'll have a functional RedwoodJS application capable of:
How to Set Up Your RedwoodJS Project and Configure Sinch
Start by creating a new RedwoodJS project and setting up the necessary environment variables and dependencies.
1. Create RedwoodJS Project:
Open your terminal and run the following command:
Follow the prompts. Choose JavaScript or TypeScript based on your preference (this guide uses JavaScript examples, but the concepts apply equally to TypeScript). Select your preferred database (PostgreSQL recommended).
2. Install Sinch SDK:
Install the Sinch Node.js SDK to interact with their API.
@sinch/sdk-core
: The official Sinch SDK for Node.js.cross-fetch
: Often required as a peer dependency or for environments wherefetch
isn't native.3. Environment Variables:
RedwoodJS uses a
.env
file for environment variables. Create one in the project root (redwood-sinch-messaging/.env
):DATABASE_URL
: Crucially, update this with the connection string for your chosen database (PostgreSQL, MySQL, SQLite, etc.). The example provided is for a local PostgreSQL setup.SINCH_PROJECT_ID
,SINCH_KEY_ID
,SINCH_KEY_SECRET
: Replace theYOUR_…
placeholders with your actual credentials found in your Sinch Dashboard underAccess Keys
. You might need to create a new API key pair. TheSINCH_KEY_SECRET
is only shown once upon creation – save it securely.SINCH_NUMBER
: ReplaceYOUR_SINCH_VIRTUAL_NUMBER
with the virtual phone number you acquired from Sinch, formatted in E.164 (e.g.,+12223334444
). Find it underNumbers
>Your virtual numbers
in the dashboard. Ensure this number is SMS-enabled and configured for the correct region/campaign if necessary (e.g., 10DLC in the US).SINCH_WEBHOOK_SECRET
: ReplaceYOUR_STRONG_RANDOM_WEBHOOK_SECRET
with a unique, cryptographically strong random string that you generate. This secures your webhook endpoint.4. Initialize Sinch Client (Utility):
Create a utility function to avoid repeating client initialization.
Create
api/src/lib/sinch.js
:This utility safely initializes the Sinch client using environment variables and ensures only one instance is created (singleton pattern). It includes basic error handling for missing or placeholder credentials.
Design Your Database Schema for Message Storage
Define a Prisma schema to store messages.
1. Define Prisma Schema:
Open
api/db/schema.prisma
and add aMessage
model:externalId
: Stores the unique ID assigned by Sinch to the message. Useful for correlating status updates later.direction
: Tracks whether the message was sent from your app (OUTBOUND
) or received by your app (INBOUND
).status
,sentAt
,receivedAt
: Timestamps and status information.2. Create Database Migration:
Apply the schema changes to your database:
Enter a name for the migration when prompted (e.g.,
add_message_model
). This command generates SQL migration files and applies them to your database.Implement Outbound SMS Sending with GraphQL
Build the service and GraphQL mutation to send SMS messages.
1. Create Redwood Service:
Generate a service to handle message logic:
This creates
api/src/services/messages/messages.js
,messages.scdl.js
, and test files.2. Implement
sendSms
Service Function:Edit
api/src/services/messages/messages.js
:requireAuth
(you'll need to set up Redwood Auth if you haven't). Customize roles as needed.SINCH_NUMBER
. Add more robust validation.try…catch
to handle potential errors during DB operations or Sinch API calls. Logs errors using Redwood's logger.Message
record before sending and updates it with the Sinch ID (response.id
from the batch send) and status upon success or failure. This ensures you have a record even if the Sinch call fails.sinchClient.sms.batches.send
to send the message.PENDING
, updates toSENT
(orACCEPTED
) if Sinch accepts the request, andFAILED
on error. Note thatSENT
here means "accepted by Sinch," not necessarily "delivered to handset." True delivery status requires setting up Delivery Report webhooks (an advanced topic).3. Define GraphQL Schema:
Edit
api/src/graphql/messages.sdl.js
to define the types and mutation:Message
type mirroring the Prisma model.Direction
enum.Query
types (protected by@requireAuth
).SendSmsInput
type for the mutation payload.sendSms
mutation, also protected by@requireAuth
.4. Test Sending SMS (GraphQL Playground):
Start your development server:
yarn rw dev
Navigate to
http://localhost:8911/graphql
(or your configured GraphQL endpoint).Use the following mutation (replace the placeholder
to
number with a real E.164 formatted number you can check):Execute the mutation. Check your console logs (
api
side) for success or error messages. Verify the recipient phone received the SMS. Check your database to see the newMessage
record.Build Secure Webhook Handler for Inbound SMS
Set up the webhook handler to receive incoming messages. Sinch uses its Conversation API webhooks for inbound messages across various channels, including SMS.
1. Create Redwood Function:
Redwood Functions are ideal for handling webhooks. Generate one:
This creates
api/src/functions/inboundSms.js
.2. Implement Webhook Handler Logic:
Edit
api/src/functions/inboundSms.js
:verifySinchSignature
. This is critical. It uses the correct Sinch signature format:HMAC-SHA256(rawBody + '.' + nonce + '.' + timestamp)
based on research. Strongly emphasized the unreliability of theJSON.stringify
workaround for raw body access in serverless environments and recommended alternatives for production. Added checks for placeholder secret and invalid timestamps.MESSAGE_INBOUND
,INCOMING
direction, and presence oftext_message
. Extracts relevant fields. Added checks for missing critical fields.externalId
to prevent storing duplicate messages if Sinch retries a webhook.Message
record withdirection: 'INBOUND'
.500
status code, potentially prompting Sinch to retry. Returns400
for malformed payloads.200 OK
quickly.3. Expose Local Endpoint with
ngrok
:Let Sinch send webhooks to your local development machine using
ngrok
.Make sure your Redwood dev server is running (
yarn rw dev
). The API server typically runs on port 8911.Open a new terminal window and run:
ngrok
will display forwarding URLs. Copy thehttps://
URL (e.g.,https://<random-string>.ngrok-free.app
).4. Configure Sinch Webhook:
ngrok
https://
URL, appending the path to your function:/.redwood/functions/inboundSms
. The full URL will look like:https://<random-string>.ngrok-free.app/.redwood/functions/inboundSms
SINCH_WEBHOOK_SECRET
you defined in your.env
file (make sure it's not the placeholder!).MESSAGE_INBOUND
(from the CONTACT section)MESSAGE_DELIVERY
for outbound status updates (advanced).CONVERSATION_START
,EVENT_INBOUND
. Start withMESSAGE_INBOUND
.5. Test Receiving SMS:
yarn rw dev
andngrok http 8911
are running.SINCH_NUMBER
.yarn rw dev
). You should see logs from theinboundSms
function. Look for "Sinch webhook signature verified successfully" and "Inbound message stored successfully."Message
record withdirection: 'INBOUND'
has been created.ngrok
console (http://localhost:4040
) for incoming requests and responses (especially the status code returned by your function). Check the Redwood API logs for detailed errors (pay close attention to signature verification failures or errors during database operations).Error Handling, Logging, and Retry Mechanisms
Error Handling: The provided code includes basic
try…catch
blocks. For production, expand this:externalId
).Logging: Redwood's built-in
logger
is used. Configure log levels (trace
,debug
,info
,warn
,error
,fatal
) appropriately for different environments inapi/src/lib/logger.js
. Ensure sensitive data (like full message bodies if required by privacy rules) is redacted or handled carefully in logs. Structure logs as JSON (JavaScript Object Notation) for easier parsing by log aggregation tools.Retries (Outbound): For transient network errors when calling the Sinch API, implement a simple retry mechanism. Libraries like
async-retry
can help.Retries (Inbound): Sinch handles webhook retries based on the HTTP status code you return. Returning
5xx
signals an error and prompts Sinch to retry (check Sinch docs for their exact retry policy). Returning2xx
confirms receipt. Returning4xx
(like401
for bad signature or400
for bad payload) usually tells Sinch not to retry. Ensure your handler is idempotent – processing the same webhook multiple times should not cause duplicate data or unintended side effects (theexternalId
check helps with this).Security Features and Best Practices
Frequently Asked Questions About Sinch Two-Way SMS with RedwoodJS
What's the difference between Sinch SMS API and Conversation API for webhooks?
The Sinch SMS API provides dedicated SMS webhooks for Mobile Originated (MO) messages and delivery reports, storing inbound messages for 14 days. The Conversation API offers unified webhooks across multiple channels (SMS, WhatsApp, Messenger) with the
MESSAGE_INBOUND
event. For SMS-only applications, the SMS API webhooks are simpler. For multi-channel messaging, use the Conversation API webhooks. This guide uses Conversation API webhooks for future extensibility.How do I verify Sinch webhook signatures correctly in serverless environments?
Sinch webhooks use HMAC-SHA256 signature verification with the format:
HMAC-SHA256(rawBody + '.' + nonce + '.' + timestamp, secret)
. The signature appears in thex-sinch-webhook-signature
header, with nonce inx-sinch-webhook-signature-nonce
and timestamp inx-sinch-webhook-signature-timestamp
. Standard serverless platforms (Netlify, Vercel) parse JSON payloads before your function runs, making the raw body inaccessible. For production, configure your deployment to provide raw request bodies or use a custom server setup with middleware that preserves the raw body before JSON parsing.What ngrok alternatives work for testing webhooks in 2025?
Popular ngrok alternatives for webhook testing include Localtunnel (simple, free, no signup required), Pinggy (QR codes, built-in request inspection), Cloudflare Tunnel (enterprise-grade security, no bandwidth limits on free tier), and localhost.run (client-less, instant SSH tunneling). Open-source options include frp (Fast Reverse Proxy) for flexibility, Expose (PHP/Laravel-focused), and Tunnelmole (self-hosted option). Choose based on your security requirements, protocol support needs, and whether you prefer managed services or self-hosted solutions.
How do I prevent duplicate message storage from webhook retries?
Implement idempotency checks using the
externalId
field (Sinch's unique message ID). Before storing an inbound message, query your database withdb.message.findUnique({ where: { externalId } })
. If a record exists, return HTTP 200 with a "duplicate ignored" message without creating a new database entry. This ensures Sinch's automatic retries (triggered by 5xx responses or timeouts) don't create duplicate records. The unique constraint on theexternalId
column provides database-level protection as a fallback.Do I need authentication for GraphQL mutations in production?
Yes, always require authentication for production GraphQL mutations that send SMS. Use RedwoodJS's
@requireAuth
directive on yoursendSms
mutation and implement role-based access control withrequireAuth({ roles: ['admin', 'sender'] })
in your service function. This prevents unauthorized users from sending SMS messages through your application, which could lead to unexpected costs or abuse. Configure RedwoodJS Auth with your preferred provider (Auth0, Netlify Identity, Supabase Auth, etc.) before deploying to production.Summary: Production-Ready Two-Way SMS Integration
This guide provides a comprehensive walkthrough for integrating Sinch SMS capabilities into a RedwoodJS application, enabling both outbound and inbound (two-way) messaging. You've learned how to:
This foundation is ready for production use, with proper authentication, error handling, and webhook security in place. You can now expand this basic system with features like message templates, delivery reports, and enhanced security measures as needed for your application.