npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@smsmode/rcs

v1.0.1

Published

Official TypeScript SDK for smsmode© REST API RCS

Readme

@smsmode/rcs

Official TypeScript SDK for the smsmode© REST API: integrate rich RCS messaging in under 5 minutes.

npm Node.js


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/rcs

Requirements: 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/catch in 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:

  1. Log in to your smsmode customer area
  2. Go to Settings > API Keys
  3. 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-Key header: 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.

Register my RCS agent →

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 (status SCHEDULED). 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 recipients

With 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 page

With 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 breakdown

Modifying 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() and cancel() only work on scheduled campaigns (status SCHEDULED). 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.suggestions for CARD, contents[].suggestions for each card of a CAROUSEL): max 4 suggestions. These buttons are attached directly to the card.
  • Below the whole message (body.suggestions for TEXT, CARD and CAROUSEL): 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

  1. 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.
  2. Per channel: via the Channel API, if you have several channels and want a different URL per channel.
  3. Per message: the callbackUrlStatus (DLR) and callbackUrlMo (MO) fields directly in send(), 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 type
  • isDeliveryReport(payload): type guard: returns true if direction === 'MT'
  • isIncomingMessage(payload): type guard: returns true if direction === '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 responds 200 with the ENROUTE status, then sends you an UNDELIVERABLE DLR. 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


Changelog

See CHANGELOG.md for the version history.


License

MIT — see LICENSE