@smsmode/sms
v1.0.0
Published
Official TypeScript SDK for smsmode© REST API SMS
Maintainers
Readme
@smsmode/sms
Official TypeScript SDK for the smsmode© REST API. Integrate SMS sending in under 5 minutes.
Overview
This SDK is an abstraction layer on top of the smsmode© REST API. It handles HTTP headers, URL parameter encoding, error normalization, and timeouts for you, and exposes a fully-typed TypeScript interface with complete IDE autocompletion.
What the SDK covers:
- Sending single SMS, batch SMS, and scheduled SMS
- Managing grouped SMS campaigns
- Receiving and discriminating webhooks (DLR and MO)
- Typed error handling (401, 429, etc.)
- Configurable timeout with clean cancellation via
AbortController
What the SDK does not cover:
- Automatic retry: this is intentional. 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.
⚠️ Server-side only. This SDK is designed for Node.js. Never bundle it in a frontend application or browser: your API key would be publicly exposed.
Installation
npm install @smsmode/smsRequirements: Node.js >= 20.0.0
The SDK uses native fetch and has zero runtime dependencies. Nothing else to install.
ESM (recommended)
If your project uses "type": "module" in its package.json or .mjs files:
import { SmsmodeClient } from '@smsmode/sms';CommonJS
If your project uses require() (.js files without "type": "module", or .cjs):
const { SmsmodeClient } = require('@smsmode/sms');The package ships both formats. Node.js automatically picks the right one for your environment.
Quick Start
The minimal example to send your first SMS:
import { SmsmodeClient } from '@smsmode/sms';
const client = new SmsmodeClient({ apiKey: 'your-api-key' });
const message = await client.send({
recipient: { to: '33600000001' },
body: { text: 'Hello from smsmode!' },
});
console.log(message); // full API responseWrap your calls in a
try/catchin production. See the Error handling section.
Configuration
const client = new SmsmodeClient({
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 in milliseconds to wait for a response. Once exceeded, the request is cancelled and an error is thrown. Increase this value if you are sending large batches.
Obtaining and managing your API key
Where to find it:
- Log in to your smsmode account
- 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 with dotenv or your environment manager
const client = new SmsmodeClient({ apiKey: process.env.SMSMODE_API_KEY! });Role-based permissions:
Each API key is tied to a role in your smsmode organization:
| Role | Scope |
|------|-------|
| User | Send within their own Channel only |
| Manager | Send within all Channels of their organization. To target a specific Channel, use the /channels/{channelId}/messages, /channels/{channelId}/campaigns endpoints (SDK channelId option) |
| Administrator | Send within Channels of sub-organizations. To target a specific Channel, use the /channels/{channelId}/messages, /channels/{channelId}/campaigns endpoints (SDK channelId option) |
If you receive a 401 error, check that:
- The key is copied in full (no leading/trailing spaces)
- The key has not been revoked in the account interface
- You are using the
X-Api-Keyheader (the SDK handles this automatically, but it is a common issue when testing via curl or Postman)
Phone number format
All phone numbers must be in E.164 format: country code followed by the local number, no spaces, no dashes, no leading zero. The + prefix is optional; both formats are accepted by the API.
| 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 + prefix is accepted
{ to: '+33600000001' }
// Incorrect — do not include the leading zero of the local number
{ to: '0600000001' }
// Incorrect — no spaces or dashes
{ to: '33 6 00 00 00 01' }A malformed number will result in a 400 Bad Request error from the API.
SMS Messages
A Message is a single or batch SMS send. It is the base resource of the smsmode API. Each message is linked to a Channel (your sending line) and can be associated with a Campaign for statistical tracking.
Channel in Marketing mode: if your Channel is configured in Marketing mode in smsmode, the STOP mention is added automatically — you do not need to do anything.
Send an SMS
Minimum required:
await client.send({
recipient: { to: '33600000001' },
body: { text: 'Your message' },
});With a custom sender. Up to 11 characters, no spaces. A custom sender disables the recipient's ability to reply.
await client.send({
recipient: { to: '33600000001' },
body: { text: 'Your message' },
from: 'MyCompany',
});Marketing SMS with STOP opt-out. In France, commercial SMS must include a STOP mention (legal requirement). Pass stop: true and the SDK appends STOP au XXXXX automatically.
await client.send({
recipient: { to: '33600000001' },
body: { text: 'Special offer -20% this weekend!', stop: true },
from: 'MyCompany',
});Unicode encoding. By default, messages are sent in GSM7 (160 characters per segment, basic Latin). For emojis, extended accents, or non-Latin alphabets, pass encoding: 'UNICODE' — segments are then 70 characters.
await client.send({
recipient: { to: '33600000001' },
body: {
text: 'Thank you for your order 🎉 See you soon!',
encoding: 'UNICODE',
},
});Scheduled SMS. Sent automatically at sentDate. Can be modified or cancelled before sending via update() or cancel().
await client.send({
recipient: { to: '33600000001' },
body: { text: 'Reminder: your appointment is tomorrow at 10am.' },
sentDate: '2026-06-01T10:00:00Z', // ISO 8601, UTC
});Per-message webhook callbacks. Overrides the global webhook configuration for this send. callbackUrlStatus receives delivery reports (DLR), callbackUrlMo receives incoming replies (MO).
await client.send({
recipient: { to: '33600000001' },
body: { text: 'Your message' },
callbackUrlStatus: 'https://your-server.com/webhook/dlr',
callbackUrlMo: 'https://your-server.com/webhook/mo',
});Batch send
Batch allows you to send up to 1000 SMS in a single API call, which is far more efficient than calling send() in a loop. Each message in the array is independent and can have its own content, sender, and options.
const messages = await client.send([
{ recipient: { to: '33600000001' }, body: { text: 'Hello Alice!' } },
{ recipient: { to: '33600000002' }, body: { text: 'Hello Bob!' } },
]);
// The response is an array in the same order as the request
// Some items may contain individual validation errors
console.log(messages); // SmsMessage[]To send to more than 1000 recipients, use Campaigns or split into multiple batches.
List messages
// Simple list — 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); // messages on the current page
// With explicit pagination
const page2 = await client.list({ page: 2, pageSize: 50 });
// With search filters and sorting
const filtered = await client.list({
page: 1,
pageSize: 20,
searchBy: {
direction: 'MT', // MT = outbound, MO = inbound
to: '33600000001', // filter by recipient
},
sortBy: { sentDate: 'DESC' }, // most recent first
});Get a message
// Useful to check the status of a specific message
const message = await client.get('67c15045-1067-4588-ba3c-737cc5051438');
console.log(message.status.value);
// Possible values: "SCHEDULED", "ENROUTE", "DELIVERED", "UNDELIVERABLE", "UNDELIVERED", "UNKNOWN"Update a scheduled SMS
A scheduled SMS can be modified as long as it has not yet been sent. Only the fields passed in the payload are updated; the rest remain unchanged.
await client.update('67c15045-1067-4588-ba3c-737cc5051438', {
body: { text: 'Updated message content' },
sentDate: '2026-06-02T09:00:00Z', // reschedule to a different date
});
// To clear an optional field, pass an empty string
await client.update('67c15045-1067-4588-ba3c-737cc5051438', {
refClient: '', // removes the client reference
});Cancel a scheduled SMS
Important:
cancel()only works on scheduled messages (statusSCHEDULED). A message that has already been sent cannot be cancelled.
// Permanently cancels the SMS — it will not be sent
await client.cancel('67c15045-1067-4588-ba3c-737cc5051438');Using a specific Channel or Campaign
By default, calls use the Channel linked to your API key. You can target a specific Channel or Campaign via the second options parameter, available on all methods.
// Send via a specific Channel (requires Manager or Administrator permissions)
await client.send(
{ recipient: { to: '33600000001' }, body: { text: 'Hello!' } },
{ channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' }
);
// Associate the message with an existing Campaign (for statistics)
await client.send(
{ recipient: { to: '33600000001' }, body: { text: 'Hello!' } },
{ campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70' }
);
// Combine both
await client.send(
{ recipient: { to: '33600000001' }, body: { text: 'Hello!' } },
{
channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd',
campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70',
}
);
// The options parameter works on all methods
await client.list({ page: 1 }, { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' });
await client.get('message-id', { campaignId: '4c9f9589-1ffd-48da-82d2-65aa9e5f5f70' });
await client.cancel('message-id', { channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' });SMS Campaigns
A Campaign is a grouped send to multiple recipients, treated as a single unit with consolidated statistics (delivery rate, send count, etc.). It is the recommended resource for marketing sends or mass notifications.
Difference from Message batch:
- The batch (
client.send([...])) sends independent messages with no link between them. - The Campaign groups all sends under a common identifier (
campaignId), enabling global performance tracking.
Limit: 1000 recipients per campaign. To exceed this limit, create the campaign first, then send additional messages via client.send([...], { campaignId }).
Send a campaign
Simple campaign:
const campaign = await client.campaigns.send({
recipients: [
{ to: '33600000001' },
{ to: '33600000002' },
],
body: { text: 'Your campaign message' },
});
console.log(campaign.campaignId);
console.log(campaign.status); // "SCHEDULED" | "ONGOING" | "ENDED"Dynamic content per recipient. Variables prefixed with $ are replaced for each recipient from their parameters map.
await client.campaigns.send({
recipients: [
{ to: '33600000001', parameters: { name: 'Alice', code: 'PROMO10' } },
{ to: '33600000002', parameters: { name: 'Bob', code: 'PROMO20' } },
],
body: { text: 'Hello $name, your promo code: $code' },
from: 'MyCompany',
});Unicode encoding. Same rule as for messages: pass encoding: 'UNICODE' for content outside GSM7.
await client.campaigns.send({
recipients: [{ to: '33600000001' }, { to: '33600000002' }],
body: {
text: 'New collection available 🎁 Enjoy!',
encoding: 'UNICODE',
},
});Scheduled campaign with a send window. sentDate is when the send starts, endDate is the deadline beyond which unsent messages are cancelled.
await client.campaigns.send({
recipients: [{ to: '33600000001' }],
body: { text: 'Reminder: your appointment is tomorrow.' },
sentDate: '2026-06-01T09:00:00Z',
endDate: '2026-06-08T09:00:00Z',
});Via a specific Channel:
await client.campaigns.send(
{ recipients: [{ to: '33600000001' }], body: { text: 'Hello!' } },
{ channelId: 'da0e501d-4449-40b1-b1f9-3cd1e94031bd' }
);List campaigns
const result = await client.campaigns.list();
console.log(result.totalCount); // total number of campaigns
console.log(result.items); // campaigns on the current page
// Filter by status and sort
const scheduled = await client.campaigns.list({
searchBy: { status: 'SCHEDULED' }, // "SCHEDULED" | "ONGOING" | "ENDED"
sortBy: { sentDate: 'DESC' },
});Get, update, and cancel
Important:
update()andcancel()only work on scheduled campaigns (statusSCHEDULED). A campaign that is already ongoing or ended cannot be modified or cancelled.
// Full campaign details
const campaign = await client.campaigns.get('67c15045-1067-4588-ba3c-737cc5051438');
console.log(campaign.status); // current status
console.log(campaign.quantity); // number of recipients
// Update a scheduled campaign (before it is sent)
await client.campaigns.update('67c15045-1067-4588-ba3c-737cc5051438', {
body: { text: 'Updated content' },
sentDate: '2026-06-02T10:00:00Z',
});
// Permanently cancel a scheduled campaign
await client.campaigns.cancel('67c15045-1067-4588-ba3c-737cc5051438');Webhooks
Webhooks (called callback requests in the smsmode documentation) are HTTP notifications that smsmode sends to your server to inform you of events: SMS delivery receipt, incoming message from a user, etc.
There are two types of notifications:
DLR (Delivery Report): smsmode notifies you each time an SMS status changes (sent, delivered, failed…). Configured via callbackUrlStatus.
MO (Mobile Originated): smsmode notifies you when a user replies to one of your SMS messages. Configured via callbackUrlMo.
Both types share the same base structure (Message resource), but are distinguished by the presence of the status field (only present in DLRs).
Configuring the receiving URL
There are three ways to define the URL that will receive notifications:
- Global (recommended): via the smsmode interface under Settings > Webhooks. The simplest approach: configure a URL once and it applies to all your sends without touching any code.
- Per channel: via the Channel API, if you have multiple channels and want a different URL per channel.
- Per message:
callbackUrlStatus(DLR) andcallbackUrlMo(MO) fields directly insend(), to override the global configuration for a specific send.
Retry mechanism
Your server must respond with a valid HTTP status (2xx) to acknowledge the notification. If no response or an error response is received, smsmode automatically retries according to this schedule:
| Attempt | Delay after previous failure | |---------|------------------------------| | 1 | 30 seconds | | 2 | 2 minutes | | 3 | 10 minutes | | 4 | 1 hour | | 5 | 5 hours | | 6 (final) | 24 hours |
After the last attempt, the notification is permanently dropped. Your endpoint must therefore be idempotent: the same event may arrive multiple times, and your code must handle it without creating duplicates.
Receiving and identifying a webhook
The SDK exposes three utilities to process webhooks in a typed way:
parseWebhookPayload(body): validates that the payload is well-formed and returns a discriminated typeisDeliveryReport(payload): returnstrueif it is a DLRisIncomingMessage(payload): returnstrueif it is a MO
import express from 'express';
import {
parseWebhookPayload,
isDeliveryReport,
isIncomingMessage,
} from '@smsmode/sms';
const app = express();
app.use(express.json());
app.post('/webhook', (req, res) => {
try {
const payload = parseWebhookPayload(req.body);
if (isDeliveryReport(payload)) {
// Delivery report (DLR)
// payload.status.value contains the status: "DELIVERED", "UNDELIVERABLE"...
console.log(`Message ${payload.messageId} — status: ${payload.status.value}`);
} else if (isIncomingMessage(payload)) {
// Incoming message (MO) — the user replied to your SMS
console.log(`Reply from ${payload.from}: ${payload.body.text}`);
// originMessageId is present if the MO is a direct reply to one of your SMS
if (payload.originMessageId) {
console.log(`In reply to message: ${payload.originMessageId}`);
}
}
// Always respond with a 2xx status to acknowledge the notification
// Without this, smsmode will consider the notification failed and retry
res.sendStatus(200);
} catch {
// parseWebhookPayload throws if the payload is invalid (missing messageId field)
res.sendStatus(400);
}
});Patterns
Full pagination
The paginated response contains totalCount (global total) and items (current page). To iterate through all pages:
let page = 1;
let hasMore = true;
const allMessages = [];
while (hasMore) {
const result = await client.list({ page, pageSize: 100 });
allMessages.push(...result.items);
// If fewer items than the page size, we are on the last page
hasMore = result.items.length === result.pageSize;
page++;
}
console.log(`${allMessages.length} messages retrieved`);Error handling
Insufficient credit:
client.send()does not throw an error when credit is insufficient. The API responds200with statusENROUTE, then sends you anUNDELIVERABLEDLR. If you do not process your DLRs, you will never know.
The SDK transforms each HTTP error response into a typed TypeScript class. The central distinction is the presence or absence of a structured smsmode error body in the response, not the HTTP code itself. This determines what you can actually do with the error.
Error class hierarchy:
Error
└── SmsError
├── SmsmodeApiError — structured smsmode body present
│ ├── AuthError — HTTP 401
│ └── RateLimitError — HTTP 429 (+ retryAfter)
└── SmsmodeHttpError — no structured bodyimport {
SmsmodeClient,
SmsmodeApiError,
SmsmodeHttpError,
AuthError,
RateLimitError,
} from '@smsmode/sms';
const client = new SmsmodeClient({ apiKey: process.env.SMSMODE_API_KEY! });
try {
const message = await client.send({
recipient: { to: '33600000001' },
body: { text: 'Hello!' },
});
console.log(message.messageId); // message ID — keep it for client.get()
console.log(message.status.value); // e.g. "ENROUTE" — final delivery status comes via DLR
console.log(message.acceptedAt); // acceptance date by the API (ISO 8601, e.g. "2026-05-12T10:00:00Z")
} 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 on error.errorCode:
switch (error.errorCode) {
case '400.029': // invalid E.164 format
case '400.031': // from too long
// 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 | Extends SmsmodeApiError |
| RateLimitError | HTTP 429 with structured body | Extends SmsmodeApiError + retryAfter?: number |
| SmsmodeHttpError | HTTP error response without structured body (5xx, gateway timeout, HTML from a reverse proxy) | httpStatus, statusText |
SmsmodeApiError properties:
| 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 | Specific 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 verified before instantiating this class.
For the exhaustive list of smsmode business error codes, see the official status codes documentation.
Handling rate limits
RateLimitError exposes retryAfter, the recommended wait time in seconds provided by the API. You can implement automatic retry as needed:
import { SmsSendPayload, RateLimitError } from '@smsmode/sms';
async function sendWithRetry(payload: SmsSendPayload, 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 propagate
}
}
}API Reference
SmsmodeClient
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | — | Required. smsmode API key |
| timeout | number | 10000 | Timeout in milliseconds |
| baseUrl | string | https://rest.smsmode.com | API base URL |
client — Messages
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| send(payload, options?) | SmsSendPayload \| SmsSendPayload[] | SmsMessage \| SmsMessage[] | Send a single or batch SMS (max 1000) |
| list(params?, options?) | SmsListParams | PaginatedResponse<SmsMessage> | List messages with filters and pagination |
| get(messageId, options?) | string | SmsMessage | Get a message by ID |
| update(messageId, payload, options?) | string, SmsUpdatePayload | SmsMessage | Update a scheduled SMS |
| cancel(messageId, options?) | string | void | Cancel a scheduled SMS |
client.campaigns — Campaigns
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| send(payload, options?) | CampaignSendPayload | Campaign | Send or schedule a campaign (max 1000 recipients) |
| list(params?, options?) | CampaignListParams | PaginatedResponse<Campaign> | List campaigns with filters and pagination |
| get(campaignId, options?) | string | Campaign | Get a campaign by ID |
| update(campaignId, payload, options?) | string, CampaignUpdatePayload | Campaign | Update a scheduled campaign |
| cancel(campaignId, options?) | string | void | Cancel a scheduled campaign |
Webhooks
| Function | Parameters | Returns | Description |
|----------|------------|---------|-------------|
| parseWebhookPayload(body) | unknown | WebhookPayload | Validates and type-asserts an incoming payload. Throws if invalid. |
| isDeliveryReport(payload) | WebhookPayload | boolean | Type guard, true if DLR (delivery report) |
| isIncomingMessage(payload) | WebhookPayload | boolean | Type guard, true if MO (incoming message) |
Resources
- smsmode API documentation: complete REST API reference
- Postman collection: for manually testing endpoints
- smsmode account: manage API keys, credits, webhooks
- Support
Changelog
See CHANGELOG.md for the version history.
License
MIT — see LICENSE
