This guide provides a step-by-step walkthrough for building a feature within a Next.js application to send bulk SMS messages (broadcasts) efficiently using Amazon Simple Notification Service (SNS) and its PublishBatch
API.
Sending individual messages via API calls can become slow and costly when dealing with hundreds or thousands of recipients. AWS SNS PublishBatch
enables you to send up to 10 messages in a single API request, drastically reducing overhead and potentially lowering costs. We will build a secure Next.js API endpoint that accepts a list of phone numbers and a message, then uses the AWS SDK for JavaScript (v3) to interact with SNS for batch delivery.
Project Goals:
- Create a Next.js API route to handle bulk SMS requests.
- Integrate the AWS SDK v3 for SNS communication.
- Implement efficient message batching using
PublishBatchCommand
. - Handle potential errors and provide feedback on delivery status.
- Secure the API endpoint and AWS credentials.
- Provide a foundation for reliable broadcast messaging.
Technology Stack:
- Next.js: React framework for frontend and backend API routes.
- AWS SNS: Managed messaging service for sending SMS.
- AWS SDK for JavaScript v3: For interacting with AWS services.
- Node.js: Runtime environment.
System Architecture:
+-----------------+ +---------------------+ +-------------------+ +----------------+ +-----------+
| User Interface | ---> | Next.js API Route | ---> | AWS SDK (SNS v3) | ---> | AWS SNS Service| ---> | Recipients|
| (Optional Form) | | (/api/broadcast) | | (PublishBatch) | | (SMS Delivery) | | (Mobile) |
+-----------------+ +---------------------+ +-------------------+ +----------------+ +-----------+
| | |
| +-----------------------------+-------------> CloudWatch Logs/Metrics (Monitoring)
|
+---------------------------------------------------------------------> Secure API Call (Auth)
Prerequisites:
- An AWS account with permissions to manage SNS and IAM.
- Node.js (
v18
or later recommended) and npm/yarn installed. - Basic understanding of Next.js, React, JavaScript/TypeScript, and REST APIs.
- AWS CLI configured locally (optional but helpful for setup).
1. Project Setup
Let's initialize a new Next.js project and install the necessary dependencies.
-
Create Next.js App: Open your terminal and run:
npx create-next-app@latest sns-bulk-messaging-app cd sns-bulk-messaging-app
(Choose your preferred settings regarding TypeScript, ESLint, Tailwind,
src/
directory, App Router/Pages Router – this guide assumes Pages Router for API route simplicity, but App Router works similarly). -
Install AWS SDK: We need the SNS client package from the AWS SDK v3.
npm install @aws-sdk/client-sns
- Why AWS SDK v3? It offers modular packages, improved TypeScript support, and follows modern JavaScript practices compared to v2.
-
Environment Variables: Sensitive information like AWS credentials and the SNS Topic ARN (if used) should never be hardcoded. Create a file named
.env.local
in the root of your project.# .env.local # AWS Credentials - BEST PRACTICE: Use IAM Roles for production deployments (e.g., on EC2/ECS/Lambda) # For local development, you can use credentials from ~/.aws/credentials or these vars. AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY AWS_REGION=us-east-1 # Replace with your desired AWS region # Optional: If broadcasting via a Topic instead of direct phone numbers # SNS_TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:YourSnsTopicName
AWS_ACCESS_KEY_ID
/AWS_SECRET_ACCESS_KEY
: Credentials for an IAM user with programmatic access. Strongly recommended: For production, use IAM roles attached to your compute environment (like Vercel, EC2, Lambda) instead of long-lived keys. See Section 6 for more on security.AWS_REGION
: The AWS region where your SNS resources will be or are located (e.g.,us-west-2
,eu-central-1
).SNS_TOPIC_ARN
: (Optional) If you intend to publish to a topic where users subscribe, include its ARN. This guide focuses on direct-to-phone-number batching, which doesn't strictly require a topic ARN for thePublishBatch
command itself when specifying phone numbers directly in the entries.
-
Gitignore: Ensure
.env.local
is included in your.gitignore
file to prevent accidentally committing secrets. The default Next.js.gitignore
usually includes it.# .gitignore (ensure this line exists) .env*.local
2. AWS SNS and IAM Configuration
Before writing code, we need to configure AWS resources.
-
IAM User/Role and Permissions: You need an IAM identity (user or role) that your Next.js application can use to interact with SNS.
- Navigate to IAM: In the AWS Management Console, go to IAM.
- Create Policy:
- Go to Policies -> Create policy.
- Switch to the JSON editor.
- Paste the following policy, which grants permission to publish messages (including batches) via SNS. Replace
YOUR_REGION
,YOUR_ACCOUNT_ID
, and optionally specify a Topic ARN if you only want to allow publishing to a specific topic. For direct SMS batching, theResource: ""*""
might be necessary if not tied to a specific topic. Consult AWS documentation for the most precise permissions needed for direct SMS batch publish without a topic. For simplicity here, we grant broad publish access.{ ""Version"": ""2012-10-17"", ""Statement"": [ { ""Effect"": ""Allow"", ""Action"": [ ""sns:Publish"", ""sns:GetSMSAttributes"", ""sns:SetSMSAttributes"" ], ""Resource"": ""*"" } ] }
- Click Next, give the policy a name (e.g.,
NextJsSnsBroadcastPolicy
), and create it.
- Create IAM User (for local dev / non-ideal production):
- Go to Users -> Add users.
- Enter a username (e.g.,
nextjs-sns-app-user
). - Select ""Provide user access to the AWS Management Console"" - Uncheck this.
- Select ""Attach policies directly"".
- Search for and select the
NextJsSnsBroadcastPolicy
you just created. - Create the user.
- Go to the user's details page -> Security credentials tab -> Create access key.
- Select ""Application running outside AWS"".
- Copy the Access key ID and Secret access key immediately and store them securely (e.g., in your
.env.local
for local dev, or a secrets manager). This is the only time the secret key is shown.
- Use IAM Role (Recommended for Production): If deploying to AWS services (EC2, ECS, Lambda) or platforms like Vercel that support IAM roles, create an IAM Role instead. Attach the
NextJsSnsBroadcastPolicy
to the role, and configure your deployment environment to use this role. This avoids storing long-lived access keys in your application environment.
-
SNS Configuration (Optional but Recommended):
- SMS Sending Settings: In the AWS Console, navigate to SNS -> Text messaging (SMS). Review settings like:
- Account spending limit: Set a reasonable limit to prevent unexpected costs.
- Default message type: Choose 'Transactional' (for critical messages like OTPs, alerts) or 'Promotional' (for marketing). Transactional often has higher deliverability but stricter content rules. You can override this per message if needed using message attributes. This guide assumes 'Transactional' for reliability.
- Sender ID: Depending on the destination country, you might need to register a Sender ID or use specific number types (Long Code, Short Code, Toll-Free). Research requirements for your target audience.
- SMS Sending Settings: In the AWS Console, navigate to SNS -> Text messaging (SMS). Review settings like:
3. Implementing Core Batch Messaging Logic
Let's create the core function responsible for batching and sending messages.
-
Create SNS Client Utility: Create a file
lib/snsClient.js
to initialize and export the SNS client instance.// lib/snsClient.js import { SNSClient } from "@aws-sdk/client-sns"; // Ensure environment variables are loaded (Next.js does this automatically for .env.local) const region = process.env.AWS_REGION; const accessKeyId = process.env.AWS_ACCESS_KEY_ID; const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; if (!region) { // Region is always required console.error("AWS_REGION environment variable is not set."); // Potentially throw an error or handle appropriately depending on deployment strategy } if (!accessKeyId || !secretAccessKey) { // In production, prefer IAM Roles, so keys might be undefined. // The SDK will automatically attempt to find credentials in the environment (e.g., instance profile). console.warn("AWS Access Key ID or Secret Access Key might be missing. Ensure IAM Role is configured for production or keys are set for local dev."); } const snsClientConfig = { region: region, // Only provide credentials if they are explicitly set in the environment. // If running with an IAM Role (e.g., on EC2/Lambda/Vercel), omit credentials // and the SDK will automatically use the role's permissions. ...(accessKeyId && secretAccessKey && { credentials: { accessKeyId: accessKeyId, secretAccessKey: secretAccessKey, }, }), }; const snsClient = new SNSClient(snsClientConfig); export { snsClient };
- Why a separate file? It promotes reusability and keeps client initialization logic clean and centralized. It also handles credential configuration conditionally, supporting both local key-based development and production IAM roles.
-
Create Batch Publishing Helper: Create
lib/publishBatchHelper.js
. This function will take phone numbers and a message, chunk them, and send batches to SNS.// lib/publishBatchHelper.js import { PublishBatchCommand } from "@aws-sdk/client-sns"; import { snsClient } from "./snsClient"; // Import the initialized client const MAX_BATCH_SIZE = 10; // SNS PublishBatch limit const MAX_RETRIES = 3; // Max retries for failed messages within a batch const INITIAL_DELAY_MS = 200; // Initial delay for exponential backoff /** * Sends messages in batches using SNS PublishBatch. * Handles basic retries for failed messages within a batch using exponential backoff. * NOTE: This basic retry attempts all failures; production logic should differentiate * between retryable (e.g., throttling) and non-retryable (e.g., invalid number) errors. * * @param {string[]} phoneNumbers - Array of E.164 formatted phone numbers. * @param {string} message - The message content to send. * @returns {Promise<{success: {id: string, phoneNumber: string, messageId: string}[], failed: {id: string, phoneNumber: string, code: string, message: string}[]}>} - Summary of successful and failed sends. */ export const sendBulkSms = async (phoneNumbers, message) => { const allSuccessful = []; const allFailed = []; const uniqueIdPrefix = `batch-${Date.now()}`; // Unique prefix for batch entries // Chunk phone numbers into batches of MAX_BATCH_SIZE for (let i = 0; i < phoneNumbers.length; i += MAX_BATCH_SIZE) { const batchNumbers = phoneNumbers.slice(i, i + MAX_BATCH_SIZE); let currentBatchEntries = batchNumbers.map((num, index) => ({ Id: `${uniqueIdPrefix}-${i + index}`, // Unique ID within the entire operation PhoneNumber: num, // Use PhoneNumber for direct SMS Message: message, // Optional: Add MessageAttributes for SenderID, SMSType etc. // MessageAttributes: { // 'AWS.SNS.SMS.SMSType': { DataType: 'String', StringValue: 'Transactional' }, // 'AWS.SNS.SMS.SenderID': { DataType: 'String', StringValue: 'MySenderID' } // } })); let attempts = 0; let batchFailed = true; // Assume failure until proven otherwise let failedEntriesDetails = new Map(); // Store details of failures for final reporting while (attempts < MAX_RETRIES && currentBatchEntries.length > 0) { failedEntriesDetails.clear(); // Clear details for this attempt try { const command = new PublishBatchCommand({ // TopicArn: process.env.SNS_TOPIC_ARN, // Use this if publishing to a Topic instead PublishBatchRequestEntries: currentBatchEntries, }); console.log(`Attempt ${attempts + 1}: Sending batch of ${currentBatchEntries.length} messages starting with ID ${currentBatchEntries[0].Id}...`); const response = await snsClient.send(command); console.log(`Batch Response Received. Success: ${response.Successful?.length || 0}, Failed: ${response.Failed?.length || 0}`); // Process successful messages if (response.Successful) { response.Successful.forEach(success => { // Find the original entry to get the phone number const originalEntryIndex = currentBatchEntries.findIndex(entry => entry.Id === success.Id); if (originalEntryIndex !== -1) { allSuccessful.push({ id: success.Id, phoneNumber: currentBatchEntries[originalEntryIndex].PhoneNumber, messageId: success.MessageId, }); // Remove successful entry from current batch for retry logic currentBatchEntries.splice(originalEntryIndex, 1); } }); } // Identify entries that failed in this attempt and store details if (response.Failed) { response.Failed.forEach(failure => { // Only retry sender faults potentially? Or specific error codes? Be selective. // The current simplistic logic retries all failures. failedEntriesDetails.set(failure.Id, failure); console.error(`Failed Entry: ID=${failure.Id}, Code=${failure.Code}, Message=${failure.Message}, SenderFault=${failure.SenderFault}`); // Keep the failed entry in currentBatchEntries for the next retry attempt }); } // Check if any entries are left in the current batch to retry if (currentBatchEntries.length === 0) { batchFailed = false; // All entries in the original batch succeeded eventually break; // Exit retry loop for this batch } else { // Apply exponential backoff before retrying remaining entries const delay = INITIAL_DELAY_MS * Math.pow(2, attempts); console.log(`Retrying ${currentBatchEntries.length} failed entries after ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); // currentBatchEntries already contains only the failures to retry } } catch (error) { console.error(`Error sending batch (Attempt ${attempts + 1}):`, error); // If the entire batch request fails (e.g., network error, throttling), retry all entries in the current batch. const delay = INITIAL_DELAY_MS * Math.pow(2, attempts); console.log(`Retrying entire batch after ${delay}ms due to error...`); await new Promise(resolve => setTimeout(resolve, delay)); // Keep currentBatchEntries as is for the next attempt } attempts++; } // End while retry loop // After retries, any remaining entries in currentBatchEntries are considered finally failed if (batchFailed && currentBatchEntries.length > 0) { console.error(`Batch finally failed after ${MAX_RETRIES} attempts for IDs: ${currentBatchEntries.map(e => e.Id).join(', ')}`); currentBatchEntries.forEach(entry => { // Use the last known failure details or create a generic one const failureDetails = failedEntriesDetails.get(entry.Id) || { Code: 'RetryFailed', Message: `Failed after ${MAX_RETRIES} attempts`, SenderFault: true }; allFailed.push({ id: entry.Id, phoneNumber: entry.PhoneNumber, // We still have the full entry object here code: failureDetails.Code, message: failureDetails.Message, }); }); } } // End for loop iterating through chunks console.log(`Bulk SMS process completed. Success: ${allSuccessful.length}, Failed: ${allFailed.length}`); return { success: allSuccessful, failed: allFailed }; };
- Why this approach?
- Chunking: It splits potentially large lists into SNS-compatible batches of 10.
- Direct
PhoneNumber
: It uses thePhoneNumber
property withinPublishBatchRequestEntries
for direct SMS, which is common for broadcast use cases where recipients aren't necessarily subscribed to a topic. - Unique IDs: Generates unique IDs (
Id
) for each message within the batch, essential for correlating responses in thePublishBatchResult
. - Response Handling: It parses the
Successful
andFailed
arrays from the SNS response to track individual message status. - Retry Logic: Implements a basic exponential backoff retry mechanism for messages that failed within a batch. This handles transient errors. Note: This basic logic attempts to retry all failed messages; a production system should implement more sophisticated logic to differentiate between transient/retryable errors (like throttling) and permanent/non-retryable errors (like
InvalidParameterValue
for a bad phone number orOptedOut
) to avoid unnecessary retries. - Error Logging: Includes
console.log
andconsole.error
for visibility (replace with a proper logger in production).
- Why this approach?
4. Building the API Layer
Now, let's create the Next.js API route that will expose this functionality.
-
Create API Route File: Create
pages/api/broadcast.js
.// pages/api/broadcast.js import { sendBulkSms } from '../../lib/publishBatchHelper'; // Adjust path if using src/ directory // Optional: Use a validation library like zod for robust input validation // import { z } from 'zod'; // WARNING: Basic API Key Auth - Placeholder Only! // Replace this with a robust authentication mechanism (JWT, OAuth, etc.) in production. const DUMMY_API_KEY = process.env.BROADCAST_API_KEY || 'verysecretkey'; // Load from env // Optional: Zod schema for validation // const broadcastSchema = z.object({ // phoneNumbers: z.array(z.string().regex(/^\+?[1-9]\d{1,14}$/)).min(1), // E.164 format regex (basic) // message: z.string().min(1).max(1600), // SMS max length varies, 1600 is generous // }); export default async function handler(req, res) { // 1. Authentication & Authorization (PLACEHOLDER - REPLACE THIS) const apiKey = req.headers['x-api-key']; if (req.method !== 'POST') { res.setHeader('Allow', ['POST']); return res.status(405).end(`Method ${req.method} Not Allowed`); } // **THIS IS NOT PRODUCTION-READY AUTHENTICATION.** if (!apiKey || apiKey !== DUMMY_API_KEY) { console.warn('Unauthorized attempt to access broadcast API.'); return res.status(401).json({ error: 'Unauthorized: Invalid or missing API key.' }); } // In production, replace the above check with proper session, token, or service-to-service auth. // 2. Request Validation const { phoneNumbers, message } = req.body; // Basic validation (replace/enhance with Zod or similar) if (!Array.isArray(phoneNumbers) || phoneNumbers.length === 0 || typeof message !== 'string' || message.trim() === '') { return res.status(400).json({ error: 'Invalid input: ""phoneNumbers"" must be a non-empty array and ""message"" must be a non-empty string.' }); } // Add more specific validation (e.g., E.164 format check) const invalidNumbers = phoneNumbers.filter(num => !/^\+?[1-9]\d{1,14}$/.test(num)); if (invalidNumbers.length > 0) { return res.status(400).json({ error: `Invalid phone number format for: ${invalidNumbers.join(', ')}. Use E.164 format (e.g., +1xxxxxxxxxx).` }); } // Optional: Use Zod for stricter validation // try { // broadcastSchema.parse(req.body); // } catch (error) { // return res.status(400).json({ error: 'Invalid input', details: error.errors }); // } // 3. Call Core Logic try { console.log(`Received broadcast request for ${phoneNumbers.length} numbers.`); const result = await sendBulkSms(phoneNumbers, message); console.log(`Broadcast API Result: Success=${result.success.length}, Failed=${result.failed.length}`); // 4. Respond // Depending on needs, you might only return a summary or include full details return res.status(200).json({ message: `Broadcast attempt finished.`, attempted: phoneNumbers.length, sentSuccessfully: result.success.length, failedToSend: result.failed.length, // Optionally include details (can be large): // successes: result.success, failures: result.failed, // Good to return failures for diagnostics }); } catch (error) { console.error('Error processing broadcast request in API handler:', error); return res.status(500).json({ error: 'Internal Server Error processing broadcast request.' }); } }
- Authentication: Includes a very basic, placeholder API key check using the
x-api-key
header. This is NOT production-ready and MUST be replaced. Use proper authentication (e.g., JWT, OAuth, session cookies, dedicated auth providers) based on your application's security requirements. Load the placeholder key from environment variables (BROADCAST_API_KEY
). - Validation: Performs basic checks on the request body (
phoneNumbers
array,message
string). Includes a commented-out example usingzod
for more robust validation, including a basic E.164 regex check. Remember tonpm install zod
if you use it. - Error Handling: Uses
try...catch
to handle errors during thesendBulkSms
call and returns appropriate HTTP status codes (400, 401, 405, 500). - Response: Returns a JSON response summarizing the outcome, including counts of successes and failures, and detailed information about the failures.
- Authentication: Includes a very basic, placeholder API key check using the
-
Testing the API Endpoint: You can use
curl
or a tool like Postman/Insomnia.-
Set API Key Env Var: Before running, make sure you add
BROADCAST_API_KEY=verysecretkey
(or your chosen key) to your.env.local
file. Restart your Next.js dev server (npm run dev
) after adding it. -
curl
Example: Replace+15551234567
,+15557654321
with valid test phone numbers (use your own mobile number for initial testing).curl -X POST http://localhost:3000/api/broadcast \ -H ""Content-Type: application/json"" \ -H ""x-api-key: verysecretkey"" \ -d '{ ""phoneNumbers"": [""+15551234567"", ""+15557654321""], ""message"": ""Hello from our Next.js broadcast system! (Test)"" }'
-
Expected Response (Success Scenario):
{ ""message"": ""Broadcast attempt finished."", ""attempted"": 2, ""sentSuccessfully"": 2, ""failedToSend"": 0, ""failures"": [] }
-
Expected Response (Partial Failure Scenario):
{ ""message"": ""Broadcast attempt finished."", ""attempted"": 2, ""sentSuccessfully"": 1, ""failedToSend"": 1, ""failures"": [ { ""id"": ""batch-1678886400000-1"", ""phoneNumber"": ""+15557654321"", ""code"": ""InvalidParameter"", ""message"": ""Invalid parameter: PhoneNumber"" } ] }
-
5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
- Error Handling Strategy:
- Use
try...catch
blocks in API handlers and helper functions. - Validate input rigorously at the API boundary (Section 4).
- Return meaningful HTTP status codes (
4xx
for client errors,5xx
for server errors). - Provide clear error messages in API responses, potentially hiding internal details in production.
- Use
- Logging:
- Use
console.log
for informational messages (request received, batch sent) andconsole.error
for errors. - Production: Integrate a structured logger (e.g., Pino, Winston). Log request IDs, batch IDs, success/failure details, and error stack traces. Send logs to a centralized service (AWS CloudWatch Logs, Datadog, Sentry).
- Example (Conceptual Pino Integration):
// Replace console.log/error with logger calls // import pino from 'pino'; // const logger = pino(); // logger.info({ batchId: command.PublishBatchRequestEntries[0].Id, count: command.PublishBatchRequestEntries.length }, 'Sending SNS batch'); // logger.error({ err: error, batchId: ... }, 'Error sending SNS batch');
- Use
- Retry Mechanisms:
- The
publishBatchHelper
includes exponential backoff for failed messages within a batch attempt. - It also retries the entire batch API call if the
snsClient.send
itself throws an error (e.g., network issue, throttling). - Improvement Needed: As noted in Section 3, the current retry logic is basic. A production-ready implementation should differentiate error types. Do not retry non-retryable errors like
InvalidParameterValue
(bad number) orOptedOut
. Only retry potentially transient errors likeThrottlingException
,InternalFailure
, orServiceUnavailable
. This requires inspecting thefailure.Code
andfailure.SenderFault
properties from theresponse.Failed
array. - Testing Errors:
- Pass deliberately invalid phone numbers (e.g., "not-a-number", "+123") to the API to test validation and SNS failure responses.
- Temporarily revoke IAM permissions to test authorization errors.
- Send rapid-fire requests to potentially trigger SNS throttling (though batching reduces the likelihood).
- Mock the
snsClient.send
method in unit tests to throw specific errors.
- The
- Log Analysis: Use CloudWatch Logs Insights (if logging to CloudWatch) to query logs for specific error codes, failed phone numbers, or batch IDs during troubleshooting.
6. Adding Security Features
- IAM Best Practices:
- Least Privilege: The IAM policy in Section 2 should be as restrictive as possible. If only using direct SMS, potentially remove permissions related to topics unless needed. Grant only
sns:Publish
. - IAM Roles: Strongly prefer IAM roles over access keys for applications deployed on AWS or Vercel. Configure your deployment environment to assume a role with the necessary SNS permissions.
- Least Privilege: The IAM policy in Section 2 should be as restrictive as possible. If only using direct SMS, potentially remove permissions related to topics unless needed. Grant only
- API Endpoint Security:
- Authentication: The basic API key (Section 4) is insufficient and insecure for production. Replace it with robust authentication (JWT, OAuth, session-based, mTLS, signed requests) depending on who/what is calling the API.
- Authorization: Ensure the authenticated user/service has permission to trigger broadcasts.
- Input Validation and Sanitization:
- Use libraries like
zod
(Section 4) to strictly validatephoneNumbers
(E.164 format) andmessage
content/length. - Sanitize message content if it includes user-generated input, although SNS primarily handles text content. Be mindful of character limits and encoding.
- Use libraries like
- Rate Limiting:
- Protect your API endpoint (
/api/broadcast
) from abuse. Implement rate limiting based on IP address, API key, or user ID. Libraries likerate-limiter-flexible
or Vercel's built-in features can help. - SNS also has its own service quotas (messages per second, API requests per second).
PublishBatch
helps stay within these, but be aware of them.
- Protect your API endpoint (
- Common Vulnerabilities: While less common for a backend SMS service, ensure standard web security practices are followed (e.g., protection against DoS via large request bodies if not validated early, secure dependency management).
- Secrets Management: Store API keys (for real auth methods), AWS credentials (if not using roles), and other secrets securely using environment variables loaded into the execution environment or a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault). Ensure
.env.local
is gitignored.
7. Handling Special Cases Relevant to the Domain (SMS)
- Phone Number Formatting: Strictly enforce E.164 format (
+
followed by country code and number, e.g.,+14155552671
). The validation in Section 4 includes a basic regex. - Internationalization: SNS supports sending SMS globally, but regulations, Sender ID requirements, and costs vary significantly by country. Research the specific rules for your target countries. You might need different Sender IDs or even different AWS regions for optimal delivery.
- Opt-Out Handling: SNS automatically handles standard opt-out keywords (like STOP). When a user opts out, subsequent messages to that number from your AWS account will fail with an error like
OptedOut
. Your application should gracefully handle these failures reported byPublishBatch
(or singlePublish
), log them, and potentially update the user's status in your own database to prevent future attempts. Do not retry messages that failed due to opt-out. - Message Encoding & Length: Standard SMS is 160 characters (GSM-7 encoding). Longer messages are split into multiple segments (concatenated SMS), billed individually. Using non-GSM characters (like some emoji) forces UCS-2 encoding, reducing the limit per segment to 70 characters. Keep messages concise. SNS handles segmentation, but be aware of potential cost implications. Max message size in SNS is generally much larger but gets chunked for SMS delivery.
- Sender ID: As mentioned (Section 2), Sender ID behavior varies. Some countries require pre-registration, others allow dynamic alphanumeric IDs, and some default to a shared pool number if none is specified. Use the
AWS.SNS.SMS.SenderID
message attribute inPublishBatchRequestEntries
if needed. - Transactional vs. Promotional: Use the
AWS.SNS.SMS.SMSType
message attribute ('Transactional' or 'Promotional') if you need to override the account default per message. Ensure compliance with regulations for promotional messages (e.g., opt-in requirements, sending hours).
8. Implementing Performance Optimizations
- Batching (
PublishBatch
): This is the primary performance optimization for this use case, reducing API calls by up to 10x compared to individualPublish
calls. The core logic (Section 3) already implements this. - Asynchronous Processing: (Content cut off in original)