@smsmode/rcs
v1.0.1
Published
Official TypeScript SDK for smsmode© REST API RCS
Maintainers
Readme
@smsmode/rcs
Official TypeScript SDK for the smsmode© REST API: integrate rich RCS messaging in under 5 minutes.
Overview
This SDK is an abstraction layer on top of the smsmode© REST API. It spares you from manually handling HTTP headers, parameter encoding, errors and timeouts, and instead gives you a typed TypeScript interface with full IDE autocompletion.
What the SDK handles:
- Sending single, batch and scheduled RCS messages
- Rich content: text, file, card and carousel with interactive suggestions
- Bulk RCS campaign management (up to 1,000 recipients)
- Receiving and identifying webhooks (DLR and MO)
- Typed error handling (400, 401, 402, 429, etc.)
- Configurable timeout with clean cancellation via
AbortController
What the SDK does not handle:
- Automatic retry: this is intentional. The retry strategy depends on your business context. An example is provided in the Patterns section.
- Channel and organization management: use the smsmode interface or the API directly.
Installation
npm install @smsmode/rcsRequirements: Node.js >= 20.0.0
The SDK uses native fetch (available natively since Node.js 18, without a flag since Node.js 20) and has no runtime dependencies. Nothing else to install.
ESM (recommended)
If your project uses "type": "module" in its package.json or .mjs files, use the ES module import:
import { SmsmodeRcsClient } from '@smsmode/rcs';CommonJS
If your project uses require() (.js files without "type": "module", or .cjs), use:
const { SmsmodeRcsClient } = require('@smsmode/rcs');The package ships both formats: there's nothing to configure, Node.js automatically picks the right one for your environment.
⚠️ Server-side only. This SDK is designed to run server-side (Node.js). Never use it in code that runs in a browser: your API key would be publicly exposed in the JavaScript bundle.
Quick Start
The minimal example to send your first RCS message:
import { SmsmodeRcsClient } from '@smsmode/rcs';
const client = new SmsmodeRcsClient({ apiKey: 'your-api-key' });
const message = await client.send({
recipient: { to: '33600000001' },
body: { type: 'TEXT', text: 'Hello from smsmode RCS!' },
});
console.log(message.messageId); // unique message identifier
console.log(message.status.value); // "ENROUTE", "DELIVERED"...Remember to wrap your calls in a
try/catchin production: see the Error handling section.
Configuration
const client = new SmsmodeRcsClient({
apiKey: 'your-api-key', // Required
timeout: 10000, // Optional — default: 10,000ms
});apiKey: Your smsmode API key. See below how to obtain and manage it.
timeout: Maximum time to wait for a response, in milliseconds. Beyond this, the request is cancelled and an error is thrown. Increase this value if you send large batches.
Obtaining and managing your API key
Where to find it:
- Log in to your smsmode customer area
- Go to Settings > API Keys
- Create a new key or copy an existing one
Best practices:
Never commit your API key to your Git repository. Use an environment variable:
# .env file at the project root
SMSMODE_API_KEY=your_api_key// Load the variable with dotenv or your environment manager
const client = new SmsmodeRcsClient({ apiKey: process.env.SMSMODE_API_KEY });Permissions associated with a key:
Each API key is tied to a role in your smsmode organization:
| Role | Scope | |------|-------| | User | Send in their own Channel only | | Manager | Send in all Channels of their organization | | Administrator | Send in the Channels of sub-organizations |
If you get a 401 error, check that:
- The key is fully copied (no leading/trailing whitespace)
- The key has not been revoked in the customer area
- You are using the
X-Api-Keyheader: the SDK handles it automatically, but if you test via curl or Postman, this is a common pitfall
Registering your RCS agent
In addition to your API key, sending RCS messages requires a validated RCS agent. An RCS agent is the verified business identity that appears on the recipient's side: brand name, logo, color. Unlike a free SMS sender, an RCS agent must be registered and validated by Google before any sending.
Blocking prerequisite: no message can be sent without a validated agent. This step is done once, before any integration.
Once your agent is validated by Google and the carriers, it is automatically used for all your sends. If you have several agents and want to select a specific one, set the from field in your request with the agent name as it appears in your Channel's agent list (1-40 characters).
Phone number format
All numbers must be in E.164 format: country code followed by the local number, without +, without spaces, without dashes, without a leading zero.
| Country | Local number | E.164 format |
|---------|-------------|--------------|
| France | 06 00 00 00 01 | 33600000001 |
| Belgium | 0499 00 00 01 | 32499000001 |
| Switzerland | 079 000 00 01 | 41790000001 |
// Correct
{ to: '33600000001' }
// Also correct: the + is accepted
{ to: '+33600000001' }
// Incorrect: the local number's leading zero must not be included
{ to: '0600000001' }
// Incorrect: no spaces or dashes
{ to: '33 6 00 00 00 01' }A badly formatted number will trigger a 400 Bad Request error from the API.
RCS note: the recipient must have an RCS-compatible device and carrier. If not, the message will not be delivered: RCS has no automatic SMS fallback.
RCS Messages
A Message is a single or batch RCS send. It is the core resource of the smsmode RCS API. Each message is tied to a Channel (your RCS agent) and can be associated with a Campaign for statistical tracking.
Sending a message
Simple send — the bare minimum.
await client.send({
recipient: { to: '33600000001' },
body: { type: 'TEXT', text: 'Hello from smsmode RCS!' },
});With a custom RCS agent.
The from field lets you select the agent from your Channel's list (1-40 characters, default: "Default RCS Agent").
await client.send({
recipient: { to: '33600000001' },
body: { type: 'TEXT', text: 'Your order has shipped.' },
from: 'MyBrand',
});Scheduled message.
Set sentDate to schedule the send. The message can be modified or cancelled before sending via update() or cancel().
await client.send({
recipient: { to: '33600000001' },
body: { type: 'TEXT', text: 'Reminder: your appointment is tomorrow at 10am.' },
sentDate: '2026-06-01T10:00:00Z',
});With custom validity.
If the message cannot be delivered within this window, it expires with the UNDELIVERED status.
await client.send({
recipient: { to: '33600000001' },
body: { type: 'TEXT', text: 'Your code: 123456' },
validity: { amount: 30, timeUnit: 'SECONDS' }, // min 30s, max 48h (default)
});With client reference and callbacks.
callbackUrlStatus receives delivery reports (DLR), callbackUrlMo receives incoming replies.
await client.send({
recipient: { to: '33600000001' },
body: { type: 'TEXT', text: 'Your message' },
refClient: 'order-42',
callbackUrlStatus: 'https://your-server.com/webhook/dlr',
callbackUrlMo: 'https://your-server.com/webhook/mo',
});Batch send.
Up to 1,000 messages in a single call. Each message is independent. TypeScript overloads automatically infer RcsMessage for a single send and RcsMessage[] for a batch.
const results = await client.send([
{ recipient: { to: '33600000001' }, body: { type: 'TEXT', text: 'Hello Alice!' } },
{ recipient: { to: '33600000002' }, body: { type: 'TEXT', text: 'Hello Bob!' } },
]);To send to more than 1,000 recipients, use Campaigns or split into several batches.
Listing messages
// Returns the first page with default parameters
const result = await client.list();
console.log(result.totalCount); // total number of messages (all pages)
console.log(result.count); // number of messages on this page
console.log(result.items); // array of messages on the current page
// With pagination and filters
const filtered = await client.list({
page: 1,
pageSize: 20,
searchBy: {
direction: 'MT', // MT = sent, MO = received
to: '33600000001', // filter by recipient
},
sortBy: { sentDate: 'DESC' }, // most recent first
});Getting a message
const message = await client.get('67c15045-1067-4588-ba3c-737cc5051438');
Modifying a scheduled message
A scheduled message can be modified as long as it has not yet been sent (and at least 5 minutes before the send time). Only the fields passed in the payload are updated, the others remain unchanged.
await client.update('67c15045-1067-4588-ba3c-737cc5051438', {
sentDate: '2026-06-02T09:00:00Z',
refClient: 'order-99',
});Clearing an optional field. Pass an empty string to remove a parameter.
await client.update('67c15045-1067-4588-ba3c-737cc5051438', {
refClient: '',
});Cancelling a scheduled message
Important:
cancel()only works on scheduled messages (statusSCHEDULED). A message that has already been sent cannot be cancelled.
await client.cancel('67c15045-1067-4588-ba3c-737cc5051438');Using a specific Channel or Campaign
By default, calls use the Channel tied to your API key. You can target a specific Channel or Campaign via the second options parameter, available on every method.
// Send via a specific Channel (requires Manager or Administrator permissions)
await client.send(
{ recipient: { to: '33600000001' }, body: { type: 'TEXT', text: 'Hello!' } },
{ channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' }
);
// Associate the message with an existing Campaign (for statistics)
await client.send(
{ recipient: { to: '33600000001' }, body: { type: 'TEXT', text: 'Hello!' } },
{ campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70' }
);
// Combine both
await client.send(
{ recipient: { to: '33600000001' }, body: { type: 'TEXT', text: 'Hello!' } },
{
channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd',
campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70',
}
);
// The second options parameter works on every method
await client.list({ page: 1 }, { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' });
await client.get('message-id', { campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70' });
await client.update('message-id', { sentDate: '2026-06-02T09:00:00Z' }, { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' });
await client.cancel('message-id', { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' });RCS Campaigns
A Campaign is a bulk send to multiple recipients, treated as a coherent unit with consolidated statistics. It is the recommended resource for marketing communications or mass notifications.
Difference from a Message batch:
- The batch (
client.send([...])) sends independent messages with no link between them. - The Campaign groups all sends under a common identifier (
campaignId), which lets you track overall performance.
Limit: 1,000 recipients per campaign. To exceed this limit, create the campaign first, then send additional messages via client.send([...], { campaignId }).
Sending a campaign
Simple send — the bare minimum.
const campaign = await client.campaigns.send({
name: 'Spring Sale 2026',
recipients: [
{ to: '33600000001' },
{ to: '33600000002' },
],
body: { type: 'TEXT', text: 'Enjoy our exclusive offers this weekend!' },
});
console.log(campaign.campaignId); // campaign identifier
console.log(campaign.status); // "SCHEDULED" | "ONGOING" | "ENDED" | "CANCELED"
console.log(campaign.quantity); // number of recipientsWith rich content.
The body field accepts all RCS types: BASIC, TEXT, FILE, CARD, CAROUSEL.
await client.campaigns.send({
name: 'Spring catalog',
recipients: [{ to: '33600000001' }, { to: '33600000002' }],
body: {
type: 'CAROUSEL',
cardWidth: 'MEDIUM',
contents: [
{
title: 'New item A',
media: { fileUrl: 'https://example.com/a.jpg' },
suggestions: [{ type: 'REPLY', text: 'Learn more', postbackData: 'more_a' }],
},
{
title: 'New item B',
media: { fileUrl: 'https://example.com/b.jpg' },
suggestions: [{ type: 'REPLY', text: 'Learn more', postbackData: 'more_b' }],
},
],
},
});Scheduled campaign.
Set sentDate to schedule the send (at least 30 minutes in the future).
await client.campaigns.send({
name: 'Appointment reminder',
recipients: [{ to: '33600000001' }],
body: { type: 'TEXT', text: 'Reminder: your appointment is tomorrow.' },
sentDate: '2026-04-20T09:00:00Z',
});Via a specific Channel. Requires Manager or Administrator permissions.
await client.campaigns.send(
{ name: 'Promo', recipients: [{ to: '33600000001' }], body: { type: 'TEXT', text: 'Hello!' } },
{ channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' }
);Listing campaigns
Returns the first page with default parameters.
const result = await client.campaigns.list();
console.log(result.totalCount); // total number of campaigns
console.log(result.items); // campaigns on the current pageWith filters.
const scheduled = await client.campaigns.list({
searchBy: { status: 'SCHEDULED' }, // "SCHEDULED" | "ONGOING" | "ENDED" | "CANCELED"
sortBy: { sentDate: 'DESC' },
});Getting a campaign
const campaign = await client.campaigns.get('67c15045-1067-4588-ba3c-737cc5051438');
console.log(campaign.status); // "SCHEDULED" | "ONGOING" | "ENDED" | "CANCELED"
console.log(campaign.quantity); // total number of recipients
console.log(campaign.statuses); // delivery status breakdownModifying a scheduled campaign
Changes apply to all the individual messages of the campaign.
await client.campaigns.update('67c15045-1067-4588-ba3c-737cc5051438', {
name: 'Spring sale v2',
sentDate: '2026-04-21T10:00:00Z',
});Cancelling a scheduled campaign
Important:
update()andcancel()only work on scheduled campaigns (statusSCHEDULED). A campaign already ongoing or ended cannot be modified or cancelled.
await client.campaigns.cancel('67c15045-1067-4588-ba3c-737cc5051438');Rich content
RCS supports five content types, all typed via the RcsBody discriminated union. The type field determines the exact structure of the body: TypeScript guides you at every step through autocompletion.
BASIC: plain text (max 160 characters, no suggestions)
Ideal for simple transactional notifications.
body: { type: 'BASIC', text: 'Your parcel has been delivered.' }TEXT: rich text with suggestions (max 3,072 characters, up to 11 suggestions)
Lets you add action buttons under the message.
body: {
type: 'TEXT',
text: 'Choose a slot for your delivery:',
suggestions: [
{ type: 'REPLY', text: 'Morning', postbackData: 'slot_morning' },
{ type: 'REPLY', text: 'Afternoon', postbackData: 'slot_afternoon' },
{ type: 'OPEN_URL', text: 'View tracking', postbackData: 'tracking', url: 'https://example.com/tracking' },
],
}FILE: image, video, audio or PDF
// Image
body: {
type: 'FILE',
fileUrl: 'https://example.com/banner.jpg',
}
// Video with thumbnail
body: {
type: 'FILE',
fileUrl: 'https://example.com/presentation.mp4',
thumbnailUrl: 'https://example.com/thumbnail.jpg',
}CARD: rich card with media, title and suggestions
body: {
type: 'CARD',
orientation: 'VERTICAL', // or "HORIZONTAL"
content: {
title: 'Spring Sale',
description: 'Up to -50% this weekend only.',
media: { fileUrl: 'https://example.com/banner.jpg', height: 'MEDIUM' },
suggestions: [
{ type: 'OPEN_URL', text: 'View offers', postbackData: 'view', url: 'https://example.com/sale' },
{ type: 'REPLY', text: 'Remind me', postbackData: 'remind' },
],
},
suggestions: [
{ type: 'REPLY', text: 'No thanks', postbackData: 'no' },
],
}CAROUSEL: 2 to 11 scrolling cards
Ideal for showcasing multiple products or offers in a single message.
body: {
type: 'CAROUSEL',
cardWidth: 'MEDIUM', // or "SMALL"
contents: [
{
title: 'Product A',
description: '€29.99',
media: { fileUrl: 'https://example.com/product-a.jpg' },
suggestions: [
{ type: 'REPLY', text: 'Add to cart', postbackData: 'add_a' },
],
},
{
title: 'Product B',
description: '€49.99',
media: { fileUrl: 'https://example.com/product-b.jpg' },
suggestions: [
{ type: 'REPLY', text: 'Add to cart', postbackData: 'add_b' },
],
},
],
}Suggestion types
Suggestions can be placed at two levels:
- Inside a card's content (
content.suggestionsforCARD,contents[].suggestionsfor each card of aCAROUSEL): max 4 suggestions. These buttons are attached directly to the card. - Below the whole message (
body.suggestionsforTEXT,CARDandCAROUSEL): max 11 suggestions. These buttons appear below the main content.
All suggestion types share three base fields: type (the type identifier), text (the text shown on the button, max 25 characters) and postbackData (the payload sent back to the agent when the user taps, max 2048 characters, base64-encoded). The "Additional fields" column lists only the fields specific to each type.
| Type | Description | Additional fields |
|------|-------------|-------------------|
| REPLY | Predefined quick reply | none |
| OPEN_URL | Opens a URL | url, webviewSize? |
| DIAL_PHONE | Starts a call | phoneNumber |
| SHOW_LOCATION | Shows a location on the map | latitude, longitude, label? |
| REQUEST_LOCATION | Requests the recipient's location | none |
| CREATE_CALENDAR_EVENT | Creates a calendar event | startTime, endTime, title, description? |
REPLY
{ type: 'REPLY', text: 'Confirm', postbackData: 'confirm' }OPEN_URL
{ type: 'OPEN_URL', text: 'View offers', postbackData: 'offers', url: 'https://example.com/offers' }DIAL_PHONE
{ type: 'DIAL_PHONE', text: 'Call support', postbackData: 'support', phoneNumber: '33800000000' }SHOW_LOCATION
{ type: 'SHOW_LOCATION', text: 'Our store', postbackData: 'store', latitude: 48.8566, longitude: 2.3522 }REQUEST_LOCATION
{ type: 'REQUEST_LOCATION', text: 'Share my location', postbackData: 'location' }CREATE_CALENDAR_EVENT
{
type: 'CREATE_CALENDAR_EVENT',
text: 'Add to my calendar',
postbackData: 'calendar',
title: 'smsmode appointment',
startTime: '2026-06-01T10:00:00Z',
endTime: '2026-06-01T11:00:00Z',
}RCS Templates
smsmode offers a library of ready-to-use RCS templates (delivery notifications, promotions, chat flows...) to help you get started.
Discover smsmode RCS templates →
Webhooks
Webhooks are callback requests that smsmode sends to your server to notify you of events related to your RCS messages.
There are two notification types:
DLR (Delivery Report): smsmode notifies you every time the status of an RCS message changes (sent, delivered, read, failed...). Configured via callbackUrlStatus.
MO (Mobile Originated): smsmode notifies you when a user replies to one of your RCS messages. The body of an MO message is one of three types — TEXT (free text), LOCATION (coordinates shared by the user) or SUGGESTION (a suggestion the user tapped, carrying its postbackData). Configured via callbackUrlMo.
Both types share the same base structure (the Message resource) and are distinguished by the direction field: MT for a DLR, MO for an incoming message. The status field is only present in a DLR.
Configuring the receiving URL
- Global (recommended): via the smsmode interface under Settings > Webhooks. This is the simplest way: you configure a URL once and it applies to all your sends, without touching the code.
- Per channel: via the Channel API, if you have several channels and want a different URL per channel.
- Per message: the
callbackUrlStatus(DLR) andcallbackUrlMo(MO) fields directly insend(), to override the global configuration on a one-off basis.
Retry mechanism
If your endpoint does not respond with a 2xx status, smsmode retries the notification up to 6 times:
| Attempt | Delay since the previous one | |---------|------------------------------| | 1 | 30 seconds | | 2 | 2 minutes | | 3 | 10 minutes | | 4 | 1 hour | | 5 | 5 hours | | 6 | 24 hours |
After the 6th attempt, the notification is dropped. Your endpoint must handle duplicates by deduplicating on messageId.
Receiving and identifying a webhook
The SDK exposes three utilities to handle webhooks in a typed way:
parseWebhookPayload(body): validates that the payload is well-formed and returns a discriminated typeisDeliveryReport(payload): type guard: returnstrueifdirection === 'MT'isIncomingMessage(payload): type guard: returnstrueifdirection === 'MO'
Unlike Messages and Campaigns, webhooks are inbound: smsmode sends an HTTP POST to your server, so handling them is your server's job, not the SDK's. The helpers below are optional — they simply give you a validated, fully-typed payload so you don't have to parse and discriminate it by hand.
The example below uses Express. Any other Node.js server (Fastify, Hapi, Koa, raw http…) also works.
import express from 'express';
import {
parseWebhookPayload,
isDeliveryReport,
isIncomingMessage,
} from '@smsmode/rcs';
const app = express();
app.use(express.json());
app.post('/webhook/rcs', (req, res) => {
let payload;
try {
payload = parseWebhookPayload(req.body);
} catch (err) {
// Log the error so you can diagnose it (e.g. a missing express.json()).
console.error('[webhook RCS] invalid payload:', err);
res.sendStatus(400);
return;
}
// Acknowledge early with a 2xx so smsmode does not retry while you process.
res.sendStatus(200);
if (isDeliveryReport(payload)) {
// Delivery report (DLR): direction: 'MT'
switch (payload.status.value) {
case 'DELIVERED':
console.log(`Message ${payload.messageId} delivered`);
break;
case 'READ':
console.log(`Message ${payload.messageId} read`);
break;
case 'UNDELIVERABLE':
console.log(`Message ${payload.messageId} undeliverable`);
break;
default:
console.log(`Message ${payload.messageId}: status: ${payload.status.value}`);
}
} else if (isIncomingMessage(payload)) {
// Incoming message (MO): direction: 'MO'
// payload.body is an RcsIncomingBody union: narrow on body.type
switch (payload.body.type) {
case 'TEXT':
console.log(`Reply from ${payload.recipient.to}: ${payload.body.text}`);
break;
case 'LOCATION':
console.log(`Location: ${payload.body.latitude}, ${payload.body.longitude}`);
break;
case 'SUGGESTION':
// postbackData identifies which suggestion the user tapped
console.log(`Tapped "${payload.body.text}" (postbackData: ${payload.body.postbackData})`);
break;
}
// originMessageId identifies the MT message this MO replies to
console.log(`In reply to message: ${payload.originMessageId}`);
}
});Patterns
Full pagination
The paginated response contains totalCount (global total) and items (current page). To iterate through all pages:
const allMessages: RcsMessage[] = [];
let page = 1;
while (true) {
const result = await client.list({ page, pageSize: 100 });
allMessages.push(...result.items);
// Stop once every message has been collected. The extra `items.length === 0`
// guard prevents an infinite loop should the server return an unexpected count.
if (allMessages.length >= result.totalCount || result.items.length === 0) break;
page++;
}
console.log(`${allMessages.length} messages retrieved`);Rate limit handling
The SDK throws a RateLimitError on HTTP 429 responses. The error exposes retryAfter, the recommended wait time in seconds provided by the API. You can implement automatic retry as needed:
import { RcsSendPayload, RateLimitError } from '@smsmode/rcs';
async function sendWithRetry(payload: RcsSendPayload, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.send(payload);
} catch (error) {
if (error instanceof RateLimitError && attempt < maxRetries - 1) {
const waitMs = (error.retryAfter ?? 60) * 1000;
await new Promise(resolve => setTimeout(resolve, waitMs));
continue;
}
throw error; // other errors or last attempt: let it bubble up
}
}
}Error handling
Insufficient credit:
client.send()does not throw an error when credit is insufficient. The API responds200with theENROUTEstatus, then sends you anUNDELIVERABLEDLR. If you don't process your DLRs, you'll never know.
The SDK turns each HTTP error response into a typed TypeScript class. The central distinction is whether a structured smsmode error body is present in the response, not the HTTP code itself. This determines what you can actually do with the error.
Error class hierarchy:
Error
└── RcsError
├── SmsModeApiError : structured smsmode body present
│ ├── AuthError : HTTP 401
│ └── RateLimitError : HTTP 429 (+ retryAfter)
└── SmsModeHttpError : no structured body (5xx, gateway timeout, HTML from a reverse proxy)import {
SmsmodeRcsClient,
SmsModeApiError,
SmsModeHttpError,
AuthError,
RateLimitError,
} from '@smsmode/rcs';
const client = new SmsmodeRcsClient({ apiKey: process.env.SMSMODE_API_KEY });
try {
await client.send({
recipient: { to: '33600000001' },
body: { type: 'TEXT', text: 'Hello!' },
});
console.log(message.messageId);
console.log(message.status.value);
} catch (error) {
if (error instanceof AuthError) {
// Invalid API key: check process.env.SMSMODE_API_KEY
console.error('Invalid API key');
} else if (error instanceof RateLimitError) {
// Too many requests: wait retryAfter seconds
const wait = error.retryAfter ?? 60;
console.error(`Rate limit reached, retry in ${wait}s`);
} else if (error instanceof SmsModeApiError) {
console.error(" httpStatus :", error.httpStatus);
console.error(" errorCode :", error.errorCode);
console.error(" message :", error.message);
console.error(" detail :", error.detail);
console.error(" docsUrl :", error.docsUrl);
// Fine-grained branching possible on error.errorCode:
switch (error.errorCode) {
case '400.029': // invalid E.164 format
// etc.
}
} else if (error instanceof SmsModeHttpError) {
// No structured body: 5xx, gateway timeout, reverse proxy
console.error(`HTTP ${error.httpStatus} ${error.statusText}: retry recommended`);
} else {
throw error;
}
}Error classes exposed by the SDK:
| Class | When? | Properties |
|-------|-------|------------|
| SmsModeApiError | The smsmode API responded with a structured error body (mainly 4xx) | httpStatus, title, message, detail, errorCode, docsUrl, details |
| AuthError | HTTP 401, invalid API key | Inherits from SmsModeApiError |
| RateLimitError | HTTP 429 with structured body | Inherits from SmsModeApiError + retryAfter?: number |
| SmsModeHttpError | HTTP error response without a structured body (5xx, gateway timeout, HTML from a reverse proxy) | httpStatus, statusText |
Properties of SmsModeApiError:
| Property | Type | Description |
|----------|------|-------------|
| httpStatus | number | HTTP code (e.g. 400) |
| title | string | Human-readable HTTP title (e.g. "Bad Request") |
| message | string | Short error description |
| detail | string | Precise constraint that was violated |
| errorCode | string | smsmode business code (e.g. "400.029") |
| docsUrl | string | URL to the error code documentation |
| details | unknown | Full raw response body |
All fields of SmsModeApiError are guaranteed non-undefined. The presence of the structured smsmode body is checked before throwing this class.
For the exhaustive list of smsmode business error codes, see the official status codes documentation.
API Reference
SmsmodeRcsClient
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | — | Required. smsmode API key |
| timeout | number | 10000 | Timeout in milliseconds |
client: Messages
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| send(payload, options?) | RcsSendPayload \| RcsSendPayload[] | RcsMessage \| RcsMessage[] | Send a single or batch message (max 1,000) |
| list(params?, options?) | RcsListParams | PaginatedResponse<RcsMessage> | List messages with filters and pagination |
| get(messageId, options?) | string | RcsMessage | Get a message by its ID |
| update(messageId, payload, options?) | string, RcsUpdatePayload | RcsMessage | Modify a scheduled message |
| cancel(messageId, options?) | string | void | Cancel a scheduled message |
client.campaigns: Campaigns
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| send(payload, options?) | RcsCampaignSendPayload | RcsCampaign | Send or schedule a campaign (max 1,000 recipients) |
| list(params?, options?) | RcsCampaignListParams | PaginatedResponse<RcsCampaign> | List campaigns with filters and pagination |
| get(campaignId, options?) | string | RcsCampaign | Get a campaign by its ID |
| update(campaignId, payload, options?) | string, RcsCampaignUpdatePayload | RcsCampaign | Modify a scheduled campaign |
| cancel(campaignId, options?) | string | void | Cancel a scheduled campaign |
Webhooks
| Function | Parameters | Returns | Description |
|----------|------------|---------|-------------|
| parseWebhookPayload(body) | unknown | RcsWebhookPayload | Validates and type-asserts an incoming payload. Throws an error if the payload is invalid. |
| isDeliveryReport(payload) | RcsWebhookPayload | boolean | Type guard: true if DLR (direction: 'MT') |
| isIncomingMessage(payload) | RcsWebhookPayload | boolean | Type guard: true if MO (direction: 'MO') |
Resources
- smsmode API documentation: complete REST API reference
- smsmode RCS templates: library of templates and chat flows
- Postman collection: to test the endpoints manually
- smsmode customer area: API key, credit and webhook management
- Support
Changelog
See CHANGELOG.md for the version history.
License
MIT — see LICENSE
