Sending Sinch MMS Messages with NestJS: A Developer Guide
This guide provides a step-by-step walkthrough for building a production-ready NestJS application capable of sending MMS messages using the Sinch MMS JSON API. We'll cover everything from initial project setup and core functionality implementation to advanced topics like error handling, security, monitoring, and deployment.
By the end of this guide, you will have a robust NestJS service that can reliably send MMS messages, complete with logging, validation, and best practices for integration with Sinch.
Project Overview and Goals
Goal: To create a NestJS backend service that exposes an API endpoint to send MMS messages via the Sinch platform.
Problem Solved: This service enables developers to programmatically send rich media messages (images, videos, audio, contacts, etc.) as part of their application's workflows, such as notifications, marketing campaigns, or user interactions, leveraging Sinch's MMS capabilities.
Technologies Used:
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript. Its modular architecture, dependency injection, and built-in tooling make it ideal for structured backend development.
- Sinch MMS JSON API: The specific Sinch API endpoint (
sendmms
action) designed for sending individual MMS messages with detailed configuration options. - TypeScript: Provides static typing for enhanced code quality, maintainability, and developer productivity.
- Axios (via
@nestjs/axios
): A promise-based HTTP client for making requests to the Sinch API. @nestjs/config
: For managing environment variables and application configuration securely.class-validator
&class-transformer
: For robust request data validation and transformation.@nestjs/swagger
: For generating API documentation automatically.- Prisma (Optional): For database interaction, specifically logging message attempts and tracking IDs.
- Docker: For containerizing the application for consistent deployment.
System Architecture:
graph LR
Client[Client Application / Postman] -- HTTP POST Request --> A(NestJS API Endpoint /mms/send);
A -- Calls MmsService.sendMms() --> B(MmsService);
B -- Loads Config --> C(ConfigService / .env);
B -- Makes HTTP POST --> D(Sinch MMS API);
D -- Fetches Media --> E(Media URL Host);
D -- Sends Response --> B;
B -- (Optional) Logs to DB --> F(Database / Prisma);
B -- Returns Result --> A;
A -- HTTP Response --> Client;
B -- Logs Info/Errors --> G(Logger);
style D fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#ccf,stroke:#333,stroke-width:1px
style F fill:#eee,stroke:#333,stroke-width:1px
style G fill:#eee,stroke:#333,stroke-width:1px
Prerequisites:
- Node.js (LTS version recommended, e.g., v18 or v20) and npm/yarn installed.
- A Sinch account with MMS capabilities enabled. You will need:
- Your Sinch Service ID (often referred to as Project ID or Campaign ID in the context of the MMS API).
- A Sinch API Token (Bearer Token).
- A Sinch-provisioned phone number (Short code, Toll-Free, or 10DLC) capable of sending MMS.
- Basic understanding of TypeScript, NestJS concepts, and REST APIs.
- Access to a terminal or command prompt.
- (Optional) Docker installed for containerization.
- (Optional) A database (e.g., PostgreSQL) and Prisma CLI installed if implementing the database logging section.
- A publicly accessible URL hosting the media files you intend to send (Sinch needs to fetch these).
1. Setting up the Project
Let's initialize a new NestJS project and install necessary dependencies.
-
Create a New NestJS Project: Open your terminal and run the NestJS CLI command:
npx @nestjs/cli new sinch-mms-sender cd sinch-mms-sender
Choose your preferred package manager (npm or yarn) when prompted.
-
Install Dependencies: We need modules for HTTP requests, configuration management, validation, and optionally Swagger and Prisma.
# Core dependencies npm install @nestjs/axios axios @nestjs/config class-validator class-transformer # Optional for API documentation npm install @nestjs/swagger swagger-ui-express # Optional for database logging npm install @prisma/client npm install prisma --save-dev
@nestjs/axios
&axios
: For making HTTP requests to the Sinch API.@nestjs/config
: To manage environment variables securely.class-validator
&class-transformer
: For validating incoming request payloads.@nestjs/swagger
&swagger-ui-express
: (Optional) For generating OpenAPI documentation.@prisma/client
&prisma
: (Optional) For database interactions if you choose to log messages.
-
Initialize Prisma (Optional): If you plan to add database logging:
npx prisma init --datasource-provider postgresql # Or your preferred DB provider
This creates a
prisma
directory with aschema.prisma
file and a.env
file. -
Configure Environment Variables: Create or update the
.env
file in the project root. Never commit this file to version control. Add your Sinch credentials and other necessary configurations:# .env # Sinch API Credentials SINCH_API_TOKEN=YOUR_API_TOKEN_HERE SINCH_SERVICE_ID=YOUR_SERVICE_ID_HERE # IMPORTANT: Verify this base URL and the API endpoint path against Sinch documentation for your specific region/account. SINCH_BASE_URL=https://mms.api.sinch.com SINCH_FROM_NUMBER=+1xxxxxxxxxx # Your Sinch MMS-enabled number in E.164 format # Application Port PORT=3000 # Optional: Database URL (if using Prisma) # Example for PostgreSQL: DATABASE_URL=""postgresql://user:password@host:port/database?schema=public"" DATABASE_URL=""YOUR_DATABASE_CONNECTION_STRING""
- Why
.env
? It keeps sensitive credentials out of your codebase and allows for different configurations per environment (development, staging, production). - Important: Always verify the
SINCH_BASE_URL
and the specific API endpoint path (used later inMmsService
) against the official Sinch documentation for your account and region, as these can differ.
- Why
-
Load Configuration: Modify
src/app.module.ts
to load the environment variables usingConfigModule
.// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { MmsModule } from './mms/mms.module'; // We'll create this next // import { PrismaModule } from './prisma/prisma.module'; // Optional: Uncomment if using Prisma @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Make ConfigService available globally envFilePath: '.env', }), MmsModule, // PrismaModule, // Optional: Uncomment if using Prisma ], controllers: [AppController], // Keep or remove the default AppController/Service providers: [AppService], // Keep or remove the default AppController/Service }) export class AppModule {}
isGlobal: true
: MakesConfigService
injectable in any module without needing to importConfigModule
explicitly everywhere.
-
Project Structure: After completing the next steps, your structure will resemble this:
sinch-mms-sender/ ├── node_modules/ ├── prisma/ (optional) │ └── schema.prisma ├── src/ │ ├── mms/ │ │ ├── dto/ │ │ │ ├── mms-media.dto.ts │ │ │ ├── mms-slide.dto.ts │ │ │ └── send-mms.dto.ts │ │ ├── mms.controller.ts │ │ ├── mms.module.ts │ │ └── mms.service.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts ├── test/ ├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── nest-cli.json ├── package.json ├── README.md ├── tsconfig.build.json └── tsconfig.json
2. Implementing Core Functionality (MMS Service)
We'll create a dedicated module and service to handle the logic of interacting with the Sinch API.
-
Generate the MMS Module and Service: Use the NestJS CLI:
nest generate module mms nest generate service mms --flat # Use --flat to avoid creating a subdirectory
This creates
src/mms/mms.module.ts
andsrc/mms/mms.service.ts
. -
Define Data Transfer Objects (DTOs): Create DTOs to represent the structure of the data needed to send an MMS, including validation rules. Create a
src/mms/dto
directory.// src/mms/dto/mms-media.dto.ts import { IsNotEmpty, IsOptional, IsString, IsUrl } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; // Optional: For Swagger export class MmsMediaDto { @ApiProperty({ description: 'URL of the media file (image, audio, video, contact, calendar, pdf)' }) @IsUrl() @IsNotEmpty() url: string; } export class MmsSlideContentDto { @ApiPropertyOptional({ type: MmsMediaDto }) @IsOptional() image?: MmsMediaDto; @ApiPropertyOptional({ type: MmsMediaDto }) @IsOptional() audio?: MmsMediaDto; @ApiPropertyOptional({ type: MmsMediaDto }) @IsOptional() video?: MmsMediaDto; @ApiPropertyOptional({ type: MmsMediaDto }) @IsOptional() contact?: MmsMediaDto; @ApiPropertyOptional({ type: MmsMediaDto }) @IsOptional() calendar?: MmsMediaDto; @ApiPropertyOptional({ type: MmsMediaDto }) @IsOptional() pdf?: MmsMediaDto; }
// src/mms/dto/mms-slide.dto.ts import { Type } from 'class-transformer'; import { IsNotEmptyObject, IsOptional, IsString, MaxLength, ValidateNested } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { MmsSlideContentDto } from './mms-media.dto'; export class MmsSlideDto extends MmsSlideContentDto { @ApiPropertyOptional({ description: 'Text message for the slide', maxLength: 5000 }) @IsString() @MaxLength(5000) @IsOptional() 'message-text'?: string; // Note: Use quotes for property names with hyphens }
// src/mms/dto/send-mms.dto.ts import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsBoolean, IsDateString, IsISO8601, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, MaxLength, ValidateNested, ArrayMinSize, ArrayMaxSize, } from 'class-validator'; import { MmsSlideDto } from './mms-slide.dto'; export class SendMmsDto { @ApiProperty({ description: 'Destination phone number in E.164 format (e.g., +15551234567)' }) @IsPhoneNumber(null) // Use null for international format validation @IsNotEmpty() to: string; @ApiProperty({ description: 'Array of MMS slides (1-8 slides)', type: [MmsSlideDto] }) @IsArray() @ArrayMinSize(1) @ArrayMaxSize(8) @ValidateNested({ each: true }) @Type(() => MmsSlideDto) @IsNotEmpty() slide: MmsSlideDto[]; @ApiPropertyOptional({ description: 'MMS subject text', maxLength: 80 }) @IsString() @MaxLength(80) @IsOptional() 'message-subject'?: string; @ApiPropertyOptional({ description: 'Text for fallback SMS if MMS fails or is unsupported', maxLength: 160 }) @IsString() @MaxLength(160) // Keep fallback SMS concise @IsOptional() // Required only if disable-fallback-sms is false (default) 'fallback-sms-text'?: string; @ApiPropertyOptional({ description: 'Set to true to disable sending fallback SMS link', default: false }) @IsBoolean() @IsOptional() 'disable-fallback-sms-link'?: boolean = false; @ApiPropertyOptional({ description: 'Set to true to disable sending fallback SMS entirely', default: false }) @IsBoolean() @IsOptional() 'disable-fallback-sms'?: boolean = false; @ApiPropertyOptional({ description: 'ISO8601 timestamp for fallback SMS link expiration (max 1 year)', format: 'date-time' }) @IsISO8601() @IsOptional() 'fallback-sms-link-expiration'?: string; @ApiPropertyOptional({ description: 'ISO8601 timestamp for MMS delivery expiration (max 3 days)', format: 'date-time' }) @IsISO8601() @IsOptional() 'mms-expiry-timestamp'?: string; @ApiPropertyOptional({ description: 'Client-specific reference ID (max 64 chars)', maxLength: 64 }) @IsString() @MaxLength(64) @IsOptional() 'client-reference'?: string; // Note: 'from-mask' is omitted as it's often restricted and requires specific carrier approval. // Note: 'cache-content' defaults to true and usually doesn't need to be overridden. }
- Why DTOs? They define the expected shape of incoming data and leverage
class-validator
decorators for automatic validation, improving data integrity and security.@ApiProperty
is used for Swagger documentation.
- Why DTOs? They define the expected shape of incoming data and leverage
-
Implement the MMS Service: Inject
HttpService
(from@nestjs/axios
) andConfigService
intoMmsService
. Implement thesendMms
method.// src/mms/mms.service.ts import { HttpService } from '@nestjs/axios'; import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios, { AxiosError } from 'axios'; // Import axios directly for isAxiosError import { firstValueFrom } from 'rxjs'; import { SendMmsDto } from './dto/send-mms.dto'; // import { PrismaService } from '../prisma/prisma.service'; // Optional: Uncomment if using Prisma // Define a specific interface for the expected Sinch success response structure interface SinchMmsSuccessResponse { status: string; to: string; 'tracking-id': string; 'status-details'?: string; // Add other potential fields based on Sinch documentation } // Define a specific interface for the expected Sinch error response structure interface SinchMmsErrorResponse { 'error-info'?: string; message?: string; 'error-code'?: string | number; // Add other potential fields based on Sinch documentation } @Injectable() export class MmsService { private readonly logger = new Logger(MmsService.name); private readonly sinchApiToken: string; private readonly sinchServiceId: string; private readonly sinchBaseUrl: string; private readonly sinchFromNumber: string; constructor( private readonly httpService: HttpService, private readonly configService: ConfigService, // private readonly prisma: PrismaService, // Optional: Inject PrismaService ) { // Load credentials securely from config this.sinchApiToken = this.configService.get<string>('SINCH_API_TOKEN'); this.sinchServiceId = this.configService.get<string>('SINCH_SERVICE_ID'); this.sinchBaseUrl = this.configService.get<string>('SINCH_BASE_URL'); this.sinchFromNumber = this.configService.get<string>('SINCH_FROM_NUMBER'); if (!this.sinchApiToken || !this.sinchServiceId || !this.sinchBaseUrl || !this.sinchFromNumber) { this.logger.error('Sinch API credentials or configuration missing in environment variables.'); throw new InternalServerErrorException('MMS Service configuration error.'); } } async sendMms(sendMmsDto: SendMmsDto): Promise<SinchMmsSuccessResponse> { // IMPORTANT: Verify this endpoint path against Sinch documentation for your specific region/account. const apiUrl = `${this.sinchBaseUrl}/v1/projects/${this.sinchServiceId}/messages`; // Construct the payload required by the Sinch API const payload = { action: 'sendmms', 'service-id': this.sinchServiceId, from: this.sinchFromNumber, ...sendMmsDto, // Spread the validated DTO properties }; // Default fallback text if not provided and fallback is enabled if (!payload['disable-fallback-sms'] && !payload['fallback-sms-text']) { payload['fallback-sms-text'] = 'View your MMS message:'; // Provide a sensible default // Alternatively, throw a BadRequestException if it's mandatory for your use case // throw new BadRequestException('Fallback SMS text is required when fallback is enabled.'); } const headers = { 'Content-Type': 'application/json; charset=utf-8', 'Authorization': `Bearer ${this.sinchApiToken}`, }; this.logger.log(`Attempting to send MMS to ${payload.to} from ${payload.from}`); // CAUTION: Logging the full payload at debug level can expose sensitive data (recipient numbers, content URLs/text). // Use with extreme caution in production environments or remove/comment out by default. // this.logger.debug(`Sinch API Payload: ${JSON.stringify(payload)}`); try { const response = await firstValueFrom( this.httpService.post<SinchMmsSuccessResponse>(apiUrl, payload, { headers }), ); this.logger.log(`Sinch API Success Response for ${payload.to}: Status ${response.status}`); this.logger.debug(`Sinch Response Data: ${JSON.stringify(response.data)}`); // Optional: Log successful attempt to database // if (response.data?.status === 'success' && response.data?.['tracking-id']) { // Use bracket notation for hyphenated keys // try { // await this.prisma.mmsLog.create({ // data: { // recipient: payload.to, // sinchTrackingId: response.data['tracking-id'], // status: 'SUBMITTED', // Initial status // clientReference: payload['client-reference'] || null, // }, // }); // } catch (dbError) { // this.logger.error(`Failed to log MMS submission to database for ${payload.to}: ${dbError.message}`, dbError.stack); // } // } return response.data; // Return the Sinch API response body } catch (error) { this.handleSinchApiError(error, payload.to, payload); // Pass payload for logging on error } } private handleSinchApiError(error: any, recipient: string, failedPayload: any): never { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError<SinchMmsErrorResponse>; // Use the error response interface const status = axiosError.response?.status; const data = axiosError.response?.data; // Sinch often puts error details in 'error-info' or similar fields const errorMessage = data?.['error-info'] || data?.message || axiosError.message; const errorCode = data?.['error-code']; this.logger.error( `Sinch API Error for ${recipient}: Status ${status}, Code: ${errorCode}, Info: ""${errorMessage}""`, axiosError.stack ); // Log the payload that failed, being mindful of sensitive data // this.logger.debug(`Failed Request Payload: ${JSON.stringify(failedPayload)}`); // Optional: Log failed attempt to database // try { // await this.prisma.mmsLog.create({ // data: { // recipient: recipient, // sinchTrackingId: `FAILED_${Date.now()}`, // Indicate failure // status: 'FAILED_SUBMISSION', // clientReference: failedPayload['client-reference'] || null, // errorCode: errorCode?.toString() || null, // errorInfo: errorMessage || null, // }, // }); // } catch (dbError) { // this.logger.error(`Failed to log MMS failure to database for ${recipient}: ${dbError.message}`, dbError.stack); // } if (status >= 400 && status < 500) { // Client-side errors (e.g._ bad request_ auth failure) throw new BadRequestException(`Sinch API Error: ${errorMessage} (Code: ${errorCode})`); } else { // Server-side errors or network issues throw new InternalServerErrorException(`Sinch API Error: ${errorMessage} (Code: ${errorCode})`); } } else { // Non-Axios errors this.logger.error(`Unexpected error sending MMS to ${recipient}: ${error.message}`_ error.stack); // Optional: Log unexpected failure to database throw new InternalServerErrorException('An unexpected error occurred while sending MMS.'); } } }
- Why this structure? It encapsulates Sinch interaction logic_ making it reusable and testable. Configuration is loaded safely. Error handling is centralized.
firstValueFrom
converts the Observable returned byhttpService
into a Promise. - Return Type: Updated
sendMms
to returnPromise<SinchMmsSuccessResponse>
and added interfaces for better type safety. - Payload Logging: The line logging the full Sinch API payload (
this.logger.debug(...)
) remains commented out by default due to potential sensitive data exposure.
- Why this structure? It encapsulates Sinch interaction logic_ making it reusable and testable. Configuration is loaded safely. Error handling is centralized.
-
Update the MMS Module: Import and configure
HttpModule
and make theMmsService
available for injection.// src/mms/mms.module.ts import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { ConfigModule, ConfigService } from '@nestjs/config'; // Import ConfigService import { MmsService } from './mms.service'; import { MmsController } from './mms.controller'; // We'll create this next // import { PrismaModule } from '../prisma/prisma.module'; // Optional: Import if using Prisma @Module({ imports: [ HttpModule.registerAsync({ // imports: [ConfigModule], // Only needed if ConfigModule is not global useFactory: async (configService: ConfigService) => ({ timeout: configService.get<number>('HTTP_TIMEOUT', 15000), // Example: configurable timeout (increased default) maxRedirects: 5, }), inject: [ConfigService], }), // ConfigModule, // Only needed if ConfigModule is not global // PrismaModule, // Optional: Import if using Prisma ], providers: [MmsService], controllers: [MmsController], // Add the controller exports: [MmsService], // Export if needed by other modules }) export class MmsModule {}
- Why
HttpModule.registerAsync
? It allows configuring theHttpModule
dynamically, potentially using values fromConfigService
(like timeouts).
- Why
3. Building a Complete API Layer (Controller)
Expose the MMS sending functionality via a REST API endpoint.
-
Generate the MMS Controller:
nest generate controller mms --flat
This creates
src/mms/mms.controller.ts
. -
Implement the Controller Endpoint: Define a POST endpoint, inject
MmsService
, use the DTO for validation, and add Swagger decorators (optional).// src/mms/mms.controller.ts import { Controller, Post, Body, UsePipes, ValidationPipe, HttpCode, HttpStatus, Logger } from '@nestjs/common'; import { MmsService } from './mms.service'; import { SendMmsDto } from './dto/send-mms.dto'; import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; // Optional: For Swagger @ApiTags('MMS') // Optional: Group endpoints in Swagger UI @Controller('mms') export class MmsController { private readonly logger = new Logger(MmsController.name); constructor(private readonly mmsService: MmsService) {} @Post('send') @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as processing is asynchronous @UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) // Optional: Swagger Decorators @ApiOperation({ summary: 'Send an MMS message via Sinch' }) @ApiBody({ type: SendMmsDto }) @ApiResponse({ status: 202, description: 'MMS request accepted by Sinch for processing.' }) @ApiResponse({ status: 400, description: 'Bad Request - Invalid input data or Sinch client error.' }) @ApiResponse({ status: 500, description: 'Internal Server Error - Sinch API error or server issue.' }) async sendMms(@Body() sendMmsDto: SendMmsDto) { this.logger.log(`Received request to send MMS to ${sendMmsDto.to}`); try { // The service now returns the success response object const result = await this.mmsService.sendMms(sendMmsDto); // Return the tracking ID or the full success response from Sinch return result; } catch (error) { // Errors are handled and thrown by MmsService, NestJS exception filter catches them this.logger.error(`Error processing send MMS request for ${sendMmsDto.to}: ${error.message}`); throw error; // Re-throw the exception for NestJS to handle } } }
- Why
ValidationPipe
? It automatically validates the incoming request body against theSendMmsDto
rules.whitelist: true
removes properties not defined in the DTO,forbidNonWhitelisted: true
throws an error if extra properties are present, andtransform: true
attempts to convert plain objects to DTO instances. - Why
HttpStatus.ACCEPTED
? Sending an MMS is usually asynchronous. The API accepts the request, but delivery confirmation comes later (via webhooks, not covered in detail here). 202 reflects this.
- Why
-
Enable Global Validation Pipe and Swagger (Optional): Modify
src/main.ts
to enable the validation pipe globally and set up Swagger.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; // Optional: For Swagger async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); const port = configService.get<number>('PORT', 3000); const logger = new Logger('Bootstrap'); // Global Prefix (Optional) // app.setGlobalPrefix('api/v1'); // Global Validation Pipe (Alternative to @UsePipes in controller) app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, // Enable automatic transformation from payload to DTO instance transformOptions: { enableImplicitConversion: true, // Allows conversion based on TS type (e.g., string 'true' to boolean true) // Be mindful: Ensure DTO types are strict to avoid unexpected coercion. }, }), ); // Enable CORS (Configure appropriately for production) app.enableCors(); // Setup Swagger (Optional) const swaggerConfig = new DocumentBuilder() .setTitle('Sinch MMS Sender API') .setDescription('API for sending MMS messages via Sinch') .setVersion('1.0') .addBearerAuth() // If your API itself needs auth, not just Sinch .addTag('MMS') .build(); const document = SwaggerModule.createDocument(app, swaggerConfig); SwaggerModule.setup('api-docs', app, document); // Access at /api-docs await app.listen(port); logger.log(`Application listening on port ${port}`); logger.log(`API Documentation available at http://localhost:${port}/api-docs`); // Optional: Log Swagger URL } bootstrap();
enableImplicitConversion
: This option inValidationPipe
can be convenient but may lead to unexpected type coercion if your DTO property types are not strictly defined (e.g., usingany
or union types). Ensure your DTOs are robust.
-
Test with
curl
or Postman: Start the application:npm run start:dev
Use
curl
to test the endpoint (replace placeholders).curl -X POST http://localhost:3000/mms/send \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+1RECIPIENTNUMBER"", ""message-subject"": ""Your Awesome Update!"", ""fallback-sms-text"": ""Check out this image: [Link will be added automatically if enabled]"", ""slide"": [ { ""image"": { ""url"": ""https://YOUR_PUBLICLY_ACCESSIBLE_IMAGE_URL.jpg"" }, ""message-text"": ""Here is the image you requested!"" }, { ""message-text"": ""And some additional text on a second slide."" } ], ""client-reference"": ""order_update_12345"" }'
Expected Response (Success - Status Code 202):
{ ""status"": ""success"", ""to"": ""+1RECIPIENTNUMBER"", ""tracking-id"": ""UNIQUE-TRANSACTION-ID"", ""status-details"": ""MMS request accepted and queued for processing and delivery."" }
Expected Response (Validation Error - Status Code 400):
{ ""statusCode"": 400, ""message"": [ ""to must be a valid phone number"" // or other validation errors ], ""error"": ""Bad Request"" }
4. Integrating with Necessary Third-Party Services (Sinch)
This section details obtaining and configuring Sinch credentials.
-
Obtain Sinch Credentials:
- Service ID (Project ID / Campaign ID):
- Navigate to your Sinch Customer Dashboard (e.g.,
dashboard.sinch.com
). - Go to the SMS & MMS section or similar product area.
- Find your Service Plan ID or Project ID. This is often displayed prominently on the overview page or API settings section for your MMS service. This is the
SINCH_SERVICE_ID
.
- Navigate to your Sinch Customer Dashboard (e.g.,
- API Token (Bearer Token):
- In the Sinch Dashboard, navigate to Settings -> API Credentials or Access Keys.
- Generate a new API token or use an existing one. Ensure it has permissions for the MMS API.
- Securely copy the generated token. This is your
SINCH_API_TOKEN
. Treat it like a password.
- MMS-Enabled 'From' Number:
- Go to the Numbers section in your Sinch Dashboard.
- Ensure you have a number (Short Code, Toll-Free, or 10DLC) that is configured and approved for MMS sending in the target regions (e.g., US, CA).
- Copy the number in E.164 format (including the
+
and country code). This isSINCH_FROM_NUMBER
.
- Service ID (Project ID / Campaign ID):
-
Configure Environment Variables: As done in Step 1.4, place these values into your
.env
file:# .env SINCH_API_TOKEN=PASTE_YOUR_API_TOKEN_HERE SINCH_SERVICE_ID=PASTE_YOUR_SERVICE_ID_HERE # IMPORTANT: Verify this base URL and the API endpoint path against Sinch documentation for your specific region/account. SINCH_BASE_URL=https://mms.api.sinch.com SINCH_FROM_NUMBER=+1YOURSINCHNUMBER PORT=3000 DATABASE_URL=""YOUR_DATABASE_CONNECTION_STRING"" # Optional
- Verification: It is crucial to verify both the
SINCH_BASE_URL
and the specific API endpoint path (e.g.,/v1/projects/{serviceId}/messages
) used inMmsService
against the official Sinch documentation for the MMS JSON API (sendmms
action) corresponding to your specific account and region. These values can vary.
- Verification: It is crucial to verify both the
-
Secure API Keys:
.gitignore
: Ensure.env
is listed in your.gitignore
file to prevent accidental commits.- Environment Variables in Production: Use your deployment platform's mechanism for managing secrets (e.g., AWS Secrets Manager, Kubernetes Secrets, environment variables set in the hosting environment). Do not store sensitive keys directly in code or commit them to version control.