unismsgateway
v1.7.0
Published
A unified SMS gateway library that brings access to multiple SMS gateways under a single API
Maintainers
Readme
Unified SMS Gateway
Most projects rely on more than one SMS provider so they can switch if a gateway is unavailable. Each provider’s API differs, so separate integrations are usually required.
unismsgateway exposes a single API for multiple SMS gateways. You implement once, then select or switch the platform; your send flow stays the same.
Installation
npm install unismsgatewayRequirements: Node.js >= 12.0.0 (see package.json engines).
Module import
CommonJS
const unisms = require('unismsgateway');ESM / TypeScript
import * as unisms from 'unismsgateway';
// or named:
import { init, getSmsPlatform, reset, smsPlatform } from 'unismsgateway';Configuration overview
IgatewaySettings
| Field | Type | Description |
| ------------ | ----------------------------- | -------------------------------------- |
| platformId | 'route' | 'hubtel' | 'nest' | Which gateway to use. |
| param | IgatewayParam | Provider-specific options (see below). |
IgatewayParam (all fields optional except what your platformId requires)
| Field | Type | Used by | Description |
| -------------- | ------------------ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| username | string | route | Route Mobile account username. Required for route. |
| password | string | route | Route Mobile account password. Required for route. |
| host | string | route, nest | API host. See per-gateway defaults below. |
| port | number | route | TCP port for Route Mobile. Default: 8080. |
| protocol | 'http' | 'https' | route, nest | HTTPS or HTTP to the provider API. |
| clientId | string | hubtel | Hubtel client ID. Required for hubtel. |
| clientSecret | string | hubtel | Hubtel client secret. Required for hubtel. |
| apiKey | string | nest | SMSOnlineGH API key (Authorization: key …). Required for nest. |
| debug | boolean | all | If true, the active gateway logs each request/response to the console (prefix [unismsgateway:…]). Off by default. |
| keepAlive | boolean | nest | Enable HTTP keep-alive connection pooling. Reuses TCP/TLS sockets across calls, eliminating per-request handshake overhead. Stale-socket errors are recovered automatically via retries. Default: true. |
| timeout | number | nest | Request deadline in milliseconds. The request is aborted with an ETIMEDOUT error if the server does not respond within this window. Default: 10000. |
| maxSockets | number | nest | Maximum concurrent sockets in the keep-alive pool. Default: 10. |
| retries | number | nest | Automatic retry attempts on transient socket errors (ECONNRESET, ECONNABORTED, EPIPE, ETIMEDOUT). Default: 1. |
| deliveryCallback | { url: string; accept?: 'application/json' \| 'application/xml' } | nest | Optional SMSOnlineGH delivery push webhook. When set, every quickSend and send includes a callback block in the send payload. accept defaults to application/json. Receiving webhook POSTs is your app's responsibility. |
Validation runs in smsPlatform when the instance is constructed: missing required fields for the chosen platformId throw Error with a clear message.
Environment variables
There are two separate contexts. Use the section that matches what you are doing.
| Context | Who reads env? | Purpose |
| --------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Library usage (init() in your app) | Your code — this package does not read process.env. | You choose variable names and map them into platformId and param yourself. |
| Live integration test (npm test / scripts/test-live.ts) | The test script via dotenv and process.env. | Fixed names in .env (see .env.example). |
Library usage: required and optional param fields
Nothing is read from the environment unless you wire it. Required fields are determined only by platformId:
| platformId | Required in param | Optional in param (defaults in this library) |
| ------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| nest | apiKey | host (default api.smsonlinegh.com), protocol (default https), debug, keepAlive (default true), timeout (default 10000 ms), maxSockets (default 10), retries (default 1), deliveryCallback (optional delivery push webhook) |
| hubtel | clientId, clientSecret | host (default sms.hubtel.com), protocol (default https), debug, keepAlive (default true), timeout (default 10000 ms), maxSockets (default 10), retries (default 1) |
| route | username, password | host (default rslr.connectbind.com), protocol (default http), port (default 8080), debug |
Suggested env names for your app (optional; you can rename them). Credential keys (NEST_, HUBTEL_, ROUTE_*) match live test and .env.example. Platform selection differs: the test script requires GATEWAY_PLATFORM (or TEST_ALL); in your app you choose any name (the example below uses SMS_PLATFORM_ID):
| Env name (suggestion) | Maps to param | Required when platformId is |
| ---------------------- | -------------------------- | ----------------------------- |
| NEST_API_KEY | apiKey | nest |
| NEST_HOST | host | optional for nest |
| NEST_PROTOCOL | protocol | optional for nest |
| NEST_DELIVERY_CALLBACK_URL | deliveryCallback.url | optional for nest |
| NEST_DELIVERY_CALLBACK_ACCEPT | deliveryCallback.accept | optional for nest (application/json or application/xml; default application/json) |
| HUBTEL_CLIENT_ID | clientId | hubtel |
| HUBTEL_CLIENT_SECRET | clientSecret | hubtel |
| ROUTE_USERNAME | username | route |
| ROUTE_PASSWORD | password | route |
| ROUTE_HOST | host | optional for route |
| ROUTE_PORT | port (use Number(...)) | optional for route |
| ROUTE_PROTOCOL | protocol | optional for route |
You may also use names like SMSONLINEGH_API_KEY / SMSONLINEGH_HOST in your app only — there is no built-in support for alternate names in the test script; that script expects NEST_* and ROUTE_* as in .env.example.
Example wiring: branch on platformId and build param so you do not mix unrelated fields.
const unisms = require('unismsgateway');
// Pick any env name for the active gateway; the live test uses GATEWAY_PLATFORM instead.
const platformId = process.env.SMS_PLATFORM_ID;
const paramByPlatform = {
route: {
username: process.env.ROUTE_USERNAME,
password: process.env.ROUTE_PASSWORD,
host: process.env.ROUTE_HOST,
port: process.env.ROUTE_PORT ? Number(process.env.ROUTE_PORT) : undefined,
protocol: process.env.ROUTE_PROTOCOL
},
hubtel: {
clientId: process.env.HUBTEL_CLIENT_ID,
clientSecret: process.env.HUBTEL_CLIENT_SECRET
},
nest: {
apiKey: process.env.NEST_API_KEY,
host: process.env.NEST_HOST,
protocol: process.env.NEST_PROTOCOL,
deliveryCallback: process.env.NEST_DELIVERY_CALLBACK_URL
? {
url: process.env.NEST_DELIVERY_CALLBACK_URL,
accept: process.env.NEST_DELIVERY_CALLBACK_ACCEPT
}
: undefined
}
};
const gateway = unisms.init({
platformId,
param: paramByPlatform[platformId]
});Live integration test environment variables
The script scripts/test-live.ts loads .env (copy from .env.example) and expects these exact names. It does not use SMS_PLATFORM_ID or other app-specific aliases.
How a run is selected
| Variable | Required? | Description |
| ------------------ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GATEWAY_PLATFORM | Yes, unless you use TEST_ALL | Must be exactly nest, hubtel, or route. |
| TEST_ALL | Optional | If set to true, the script runs nest, then hubtel, then route in order. You still need the required variables below for each platform you run; missing keys for a step cause that step to fail. |
Per gateway — credentials and overrides
**nest (SMSOnlineGH)**
| Variable | Required? | Purpose |
| --------------- | --------- | --------------------------------------------- |
| NEST_API_KEY | Yes | Maps to param.apiKey. |
| NEST_HOST | No | Overrides default host api.smsonlinegh.com. |
| NEST_PROTOCOL | No | Overrides default https. |
| NEST_DELIVERY_CALLBACK_URL | No | Maps to param.deliveryCallback.url. When set, sends include SMSOnlineGH delivery push callback info. |
| NEST_DELIVERY_CALLBACK_ACCEPT | No | Maps to param.deliveryCallback.accept (application/json or application/xml; default application/json). |
**hubtel**
| Variable | Required? | Purpose |
| ---------------------- | --------- | ----------------------------- |
| HUBTEL_CLIENT_ID | Yes | Maps to param.clientId. |
| HUBTEL_CLIENT_SECRET | Yes | Maps to param.clientSecret. |
**route (Route Mobile)**
| Variable | Required? | Purpose |
| ---------------- | --------- | --------------------------------------------------- |
| ROUTE_USERNAME | Yes | Maps to param.username. |
| ROUTE_PASSWORD | Yes | Maps to param.password. |
| ROUTE_HOST | No | Overrides default rslr.connectbind.com. |
| ROUTE_PORT | No | Overrides default 8080 (set as a numeric string). |
| ROUTE_PROTOCOL | No | Overrides default http. |
Live send (optional; all gateways)
| Variable | Required? | Purpose |
| ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| TEST_SEND | No (default: do not send) | Set to true to run live send checks. If unset or not true, only init and balance checks run. |
| TEST_SEND_METHOD | No (default: quickSend) | quickSend — single recipient via quickSend(). send — multiple recipients via send() (nest, route). both — run both. sendPersonalized — personalised bulk via sendPersonalized() (nest only). |
| TEST_FROM | Yes when TEST_SEND=true | Sender ID (From / from). |
| TEST_TO | Yes when method is quickSend or both | Single recipient for quickSend(). Also used as a one-element array for send() when TEST_TO_MULTI is omitted. |
| TEST_TO_MULTI | Recommended for send / both | Comma-separated MSISDNs for send() (e.g. 233...,233...). Required when TEST_SEND_METHOD=send unless TEST_TO is set. |
| TEST_CONTENT | No | Message body; if omitted, the script uses a built-in default string. |
| TEST_PERSONALIZED_TEMPLATE | No (default template used) | Message template for sendPersonalized() (e.g. Hello {$name}. Your balance is ${$balance}.). |
| TEST_PERSONALIZED_DESTINATIONS | Yes when TEST_SEND_METHOD=sendPersonalized | JSON array of { to, values } objects, e.g. [{"to":"233...","values":["Name",123]}]. |
How initialization works
**init(settings: IgatewaySettings): smsPlatform** (insrc/lib/lib.ts):
- Validates and constructs a new
smsPlatformwith yoursettings. - Stores it as the module singleton (
smsPlatformInstance). - Calls
smsPlatform.init()on that instance (returns the same facade for chaining). - Returns the
smsPlatforminstance.
**smsPlatformconstructor** (insrc/lib/platform.ts):
- Runs
validateSettings()(platform id + requiredparamfields for that id). - Calls
createGateway()to instantiate the underlying provider (RouteSmsGateway,HubtelSmsGateway, orNestSmsGateway).
**getSmsPlatform(): smsPlatform | null**: Returns the current singleton, ornullifreset()was called and no newinit()has run.
There is no async bootstrap; after init() returns, quickSend, send, and sendPersonalized are ready.
Re-initializing and reset
- Switch platform or credentials: Call
**init(newSettings)** again. Each call replaces the stored singleton with a newsmsPlatform. You do not have to callreset()first. - Clear the singleton:
**reset()** sets the internal reference tonull.getSmsPlatform()then returnsnulluntil the nextinit(). Use this when you want to guarantee nothing holds a gateway instance (e.g. tests or explicit teardown).
const unisms = require('unismsgateway');
const a = unisms.init({ platformId: 'nest', param: { apiKey: 'key-1' } });
// Later: new config
const b = unisms.init({ platformId: 'hubtel', param: { clientId: 'x', clientSecret: 'y' } });
// b replaces a; unisms.getSmsPlatform() === b
unisms.reset();
// unisms.getSmsPlatform() === null
const c = unisms.init({ platformId: 'nest', param: { apiKey: 'key-2' } });Supported gateways
| platformId | Provider | Package / implementation |
| ------------ | ------------------ | --------------------------------------- |
| route | Route Mobile | routemobilesms |
| hubtel | Hubtel SMS (Ghana) | Built-in REST client (HubtelSmsGateway) |
| nest | SMSOnlineGH | Built-in REST client (NestSmsGateway) |
Configuration vs env: Required and optional param fields are summarized in Library usage: required and optional param fields. The live test runner’s .env names are listed in Live integration test environment variables.
route (Route Mobile)
Required param: username, password.
Optional param (defaults in this library):
| Field | Default if omitted |
| ---------- | ---------------------- |
| host | rslr.connectbind.com |
| protocol | 'http' |
| port | 8080 |
These are passed into routeSms from routemobilesms.
const gateway = unisms.init({
platformId: 'route',
param: {
username: 'your-username',
password: 'your-password',
host: 'rslr.connectbind.com',
protocol: 'http',
port: 8080
}
});hubtel (Hubtel)
Required param: clientId, clientSecret.
Optional param:
| Field | Default if omitted | Notes |
| ------------ | --------------------- | --------------------------------------------------------------------- |
| host | sms.hubtel.com | Override for legacy hosts (e.g. smsc.hubtel.com) if needed. |
| protocol | 'https' | |
| keepAlive | true | Set to false to open a fresh TCP/TLS connection per request. |
| timeout | 10000 | Milliseconds before the request is aborted with ETIMEDOUT. |
| maxSockets | 10 | Maximum sockets held open in the keep-alive pool. |
| retries | 1 | Retry count for transient errors (ECONNRESET, ECONNABORTED, etc). |
Requests use POST to https://sms.hubtel.com/v1/messages/... with Authorization: Basic (Base64 of clientId:clientSecret). quickSend() hits /v1/messages/send; send() uses /v1/messages/batch/simple/send; sendPersonalized() expands {$variable} placeholders locally then posts to /v1/messages/batch/personalized/send.
HubtelSmsGateway also exposes getMessageStatus(messageId) and getBatchStatus(batchId) via getGateway() for delivery status polling.
const gateway = unisms.init({
platformId: 'hubtel',
param: {
clientId: 'your-client-id',
clientSecret: 'your-client-secret'
}
});
// Optional: release pooled sockets on shutdown
gateway.getGateway().destroy();nest (SMSOnlineGH)
Required param: apiKey.
Optional param:
| Field | Default if omitted | Notes |
| ------------ | --------------------- | --------------------------------------------------------------------- |
| host | api.smsonlinegh.com | |
| protocol | 'https' | |
| keepAlive | true | Set to false to open a fresh TCP/TLS connection per request. |
| timeout | 10000 | Milliseconds before the request is aborted with ETIMEDOUT. |
| maxSockets | 10 | Maximum sockets held open in the keep-alive pool. |
| retries | 1 | Retry count for transient errors (ECONNRESET, ECONNABORTED, etc). |
| deliveryCallback | — | Optional delivery push webhook. When set, each send request includes callback: { url, accept } per SMSOnlineGH delivery push docs. accept defaults to application/json. This library registers the URL with the provider only; your app must expose and handle the webhook endpoint. |
Requests use POST to path /v5/<endpoint> (e.g. send: message/sms/send, balance: account/balance). Authorization header: Authorization: key <apiKey>.
NestSmsGateway maintains a private keep-alive connection pool (https.Agent) so TCP and TLS handshakes are paid once and subsequent requests reuse warm sockets. If the server closes an idle socket between calls and the first write fails with ECONNABORTED or ECONNRESET, the library retries automatically on a fresh socket (controlled by retries). Set keepAlive: false to revert to a per-request connection if your network environment requires it.
The destroy() method on NestSmsGateway releases all pooled sockets; call it during application shutdown so Node does not hold the event loop open. Access it via getGateway():
const gateway = unisms.init({
platformId: 'nest',
param: { apiKey: 'your-api-key' }
});
// On app shutdown:
gateway.getGateway().destroy();Example with delivery push callback:
const gateway = unisms.init({
platformId: 'nest',
param: {
apiKey: 'your-api-key',
deliveryCallback: {
url: 'https://your-app.example/sms/delivery',
accept: 'application/json' // optional; default application/json
}
}
});SMSOnlineGH POSTs delivery status to that URL asynchronously after the operator reports delivery. The initial quickSend response still only reflects submission status.
Delivery push callback payload
A delivery push notification uses the same response shape as a Message Delivery Report, but applies to one destination only. Expect a single item in data.destinations (even when the original send had multiple recipients).
The Content-Type of the POST matches your configured deliveryCallback.accept (application/json by default, or application/xml).
JSON (accept: 'application/json'):
{
"handshake": {
"id": 0,
"label": "HSHK_OK"
},
"data": {
"batch": "cfa19ba67f94fbd6b19c067b0c87ed4f",
"delivery": true,
"category": "sms",
"text": "Hello world!",
"type": 0,
"sender": "Hello",
"personalised": false,
"destinationsCount": 2,
"destinations": [
{
"to": "233246314915",
"id": "093841e5-578a-41f4-5f5f-2f3910886c12",
"country": "Ghana",
"messageCount": 1,
"submitDateTime": "2021-09-29 21:57:44",
"reportDateTime": "2021-09-29 21:57:48",
"status": {
"id": 2110,
"label": "DS_DELIVERED"
}
}
]
}
}XML (accept: 'application/xml'):
<response>
<handshake>
<id>0</id>
<label>HSHK_OK</label>
</handshake>
<data>
<batch>cfa19ba67f94fbd6b19c067b0c87ed4f</batch>
<category>sms</category>
<delivery>true</delivery>
<text>Hello world!</text>
<type>0</type>
<sender>Hello</sender>
<personalised>false</personalised>
<destinationsCount>2</destinationsCount>
<destinations>
<item>
<to>233246314915</to>
<id>093841e5-578a-41f4-5f5f-2f3910886c12</id>
<country>Ghana</country>
<messageCount>1</messageCount>
<submitDateTime>2021-09-29 21:57:44</submitDateTime>
<reportDateTime>2021-09-29 21:57:48</reportDateTime>
<status>
<id>2110</id>
<label>DS_DELIVERED</label>
</status>
</item>
</destinations>
</data>
</response>Fields your webhook handler will typically use:
| Path | Description |
| ---- | ----------- |
| handshake.label | HSHK_OK when the payload is valid. |
| data.batch | Batch ID from the original send (matches SendResult.messageId / data.batch from quickSend). |
| data.destinations[0].to | Recipient phone number for this delivery event. |
| data.destinations[0].id | Per-destination message ID. |
| data.destinations[0].status.id / status.label | Delivery outcome (e.g. 2110 / DS_DELIVERED). |
| data.destinations[0].submitDateTime | When the message was submitted to the operator. |
| data.destinations[0].reportDateTime | When the operator reported this delivery status. |
Respond with HTTP 200 promptly so SMSOnlineGH does not retry the push. Persist and process the payload asynchronously if your handler does heavy work.
Example with performance tuning:
const gateway = unisms.init({
platformId: 'nest',
param: {
apiKey: 'your-api-key',
timeout: 5000, // abort after 5 s
maxSockets: 20, // higher pool ceiling for burst traffic
retries: 2 // extra resilience on flaky networks
}
});Balance (nest only): The underlying NestSmsGateway implements getBalance(). Access it via the facade’s getGateway():
const gateway = unisms.init({
platformId: 'nest',
param: { apiKey: 'your-api-key' }
});
const nest = gateway.getGateway();
const balance = await nest.getBalance();
console.log(balance.balance, balance.model);Sending messages
Use quickSend() for a single recipient. Use send() when the active gateway supports batch delivery to multiple numbers in one request (nest, hubtel, route). Use sendPersonalized() when each recipient needs a customised message from one template (nest, hubtel). On hubtel, sendPersonalized() expands {$variable} placeholders client-side before calling Hubtel's batch personalized API.
QuickSendParams
| Field | Type | Required | Description |
| --------- | ----------------- | -------- | ---------------------------------------------------------------------- |
| From | string | yes | Sender ID or label. |
| To | string | number | yes | Recipient MSISDN or number. |
| Content | string | yes | Message body. |
| Type | number | no | Message type; nest maps this to request body type (default 0). |
camelCase: You may pass **from**, **to**, **content**, and **type** instead of the PascalCase names above. Many JavaScript projects use camelCase; if you pass only content and Content is missing, the SMSOnlineGH (nest) API receives no message body and may return handshake 1305 (MV_ERR_MESSAGE — missing or invalid message body). The library normalizes both conventions before calling the gateway.
quickSend(params, callback?)
Returns Promise<SendResult>. Optional callback is invoked with the same result when the promise completes. The params argument accepts **QuickSendParams** (PascalCase) or **QuickSendParamsCamel** ({ from, to, content, type? }). See **normalizeQuickSendParams** in the public API if you need the same mapping outside quickSend.
**SendResult:**
{
success: boolean;
messageId?: string;
data?: any;
error?: string;
statusCode?: number; // HTTP status from the provider when available (nest, etc.)
}When success is false, always read **error** — it contains a human-readable reason (provider status codes, API handshake labels, network errors, and so on). For **nest**, if the API rejects the send but returns JSON, **data** is the full parsed response body (not only response.data), so you can inspect handshake and any provider fields. For HTTP errors, data may be the raw response body string. **statusCode** is set when the adapter knows the HTTP status (for example nest).
Debugging: Set param.debug: true when calling init() to print request URLs, bodies, and responses to the console. The live test script enables debug for the nest platform so you can trace quickSend, send, and getBalance without changing application code.
Example
const unisms = require('unismsgateway');
const gateway = unisms.init({
platformId: 'nest',
param: { apiKey: 'your-api-key' }
});
async function sendSms() {
try {
const result = await gateway.quickSend({
From: 'SenderName',
To: '233XXXXXXXXX',
Content: 'Hello from unismsgateway!',
Type: 0
});
if (result.success) {
console.log('Sent:', result.messageId);
} else {
console.error('Failed:', result.error);
}
} catch (err) {
console.error(err);
}
}With callback
gateway.quickSend(
{ From: 'SenderName', To: '233XXXXXXXXX', Content: 'Test' },
(response) => {
console.log(response);
}
);SendParams
Same fields as QuickSendParams, except To is an array of recipients:
| Field | Type | Required | Description |
| --------- | ----------------------- | -------- | ---------------------------------------------------------------------- |
| From | string | yes | Sender ID or label. |
| To | (string \| number)[] | yes | One or more recipient MSISDNs or numbers. |
| Content | string | yes | Message body (same content to all recipients). |
| Type | number | no | Message type; nest maps this to request body type (default 0). |
camelCase: Pass { from, to, content, type? } where to is an array. See normalizeSendParams.
Platform behaviour
| Platform | send() support | Notes |
| -------- | ---------------- | ----- |
| nest | Yes | One HTTP request with destinations: string[]. messageId is the batch ID; per-recipient status is in data.destinations. |
| route | Yes | Passes To as number[] to Route Mobile. success is true only when every destination succeeds; partial failures set error with a summary. |
| hubtel | Yes | One HTTP request via Hubtel batch simple API. messageId is the batchId; per-recipient IDs are in data. |
send(params, callback?)
Returns Promise<SendResult>. Same result shape and callback semantics as quickSend. Accepts SendParams (PascalCase) or SendParamsCamel ({ from, to: [...], content, type? }).
Example (nest)
const result = await gateway.send({
From: 'TEST',
To: ['0246314915', '0242053072'],
Content: 'Hello from unismsgateway!',
Type: 0
});
if (result.success) {
console.log('Batch:', result.messageId);
console.log('Destinations:', result.data?.destinations);
}Personalized bulk send
Use sendPersonalized() when each recipient should receive a different message body derived from one template. Placeholders in the template use SMSOnlineGH variable syntax ({$name}, ${$balance}, etc.); per-recipient substitution values are passed as positional arrays in the same order variables appear in the template. See SMSOnlineGH Personalised Messaging.
PersonalizedSendParams
| Field | Type | Required | Description |
| --------------- | ------------------------- | -------- | --------------------------------------------------------------------------- |
| From | string | yes | Sender ID or label. |
| Content | string | yes | Message template with {$variable} placeholders. |
| Destinations | PersonalizedRecipient[] | yes | One entry per recipient with To and positional Values. |
| Type | number | no | Message type; nest maps this to request body type (default 0). |
Each PersonalizedRecipient:
| Field | Type | Required | Description |
| -------- | ----------------------- | -------- | ------------------------------------------------ |
| To | string \| number | yes | Recipient MSISDN or number. |
| Values | (string \| number)[] | yes | Substitution values in template variable order. |
camelCase: Pass { from, content, destinations: [{ to, values }], type? }. See normalizePersonalizedSendParams.
Platform behaviour
| Platform | sendPersonalized() support | Notes |
| -------- | ---------------------------- | ----- |
| nest | Yes | One HTTP request with destinations: [{ number, values }]. Same endpoint as send(); messageId is the batch ID; per-recipient status is in data.destinations. |
| route | No | Throws — use send() with the same content for all recipients. |
| hubtel | Yes | Expands {$var} template locally, then one HTTP request to Hubtel batch personalized API. messageId is the batchId. |
sendPersonalized(params, callback?)
Returns Promise<SendResult>. Same result shape and callback semantics as send. Accepts PersonalizedSendParams (PascalCase) or PersonalizedSendParamsCamel.
Example (nest)
const result = await gateway.sendPersonalized({
From: 'TEST',
Content: 'Hello {$name}. Your balance is ${$balance}.',
Destinations: [
{ To: '0246314915', Values: ['Daniel', 560.45] },
{ To: '0242053072', Values: ['Emmanuel', 348.56] }
],
Type: 0
});
if (result.success) {
console.log('Batch:', result.messageId);
console.log('Destinations:', result.data?.destinations);
}Testing (live integration)
There is no unit test suite in this package. For manual integration checks against real gateways, use the script in scripts/test-live.ts.
Setup
- Clone the repo and install dependencies:
npm install - Copy
.env.exampleto.envand set variables for your chosen platform — see Live integration test environment variables for required vs optional names per gateway. - Run:
npm test
# same as:
npm run test:liveWhat runs
- Init — Builds
paramfrom your.env, callsinit(), and checks configuration validation. - Balance — For
nestandhubtelonly, callsgetBalance()when the adapter supports it.routeskips this step. - Send — Opt-in. By default no SMS is sent. Set
TEST_SEND=trueand the send-related variables listed in Live integration test environment variables. UseTEST_SEND_METHODto choosequickSend(default),send(multi-destination onnest/hubtel/route),both, orsendPersonalized(nest/hubtel). Forroute,sendPersonalized()rejection is verified when that method is selected.
Full variable reference (selection, per-gateway credentials, live send): Live integration test environment variables. The script loads .env via dotenv (dev dependency). Exit code is 0 when all checks pass, non-zero if a step fails or no platform is selected.
API reference
| Export | Description |
| -------------------------- | -------------------------------------------------------------------------- |
| init(settings) | Create and register the singleton smsPlatform, return it. |
| getSmsPlatform() | Current smsPlatform or null after reset() and before init(). |
| reset() | Clear the singleton. |
| smsPlatform | Class type for typing/advanced use. |
| QuickSendParamsInput | Union: PascalCase QuickSendParams or camelCase QuickSendParamsCamel. |
| QuickSendParamsCamel | { from, to, content, type? } for quickSend. |
| normalizeQuickSendParams | Maps input to canonical QuickSendParams (throws if body/sender missing). |
| SendParamsInput | Union: PascalCase SendParams or camelCase SendParamsCamel. |
| SendParamsCamel | { from, to: (string\|number)[], content, type? } for send. |
| normalizeSendParams | Maps input to canonical SendParams (throws if body/sender/recipients missing). |
| PersonalizedSendParamsInput | Union: PascalCase PersonalizedSendParams or camelCase PersonalizedSendParamsCamel. |
| PersonalizedSendParamsCamel | { from, content, destinations: [{ to, values }], type? } for sendPersonalized. |
| PersonalizedRecipient | { To, Values } — one personalised destination. |
| normalizePersonalizedSendParams | Maps input to canonical PersonalizedSendParams (throws if template/sender/destinations missing). |
**smsPlatform instance methods**
| Method | Returns | Description |
| ------------------------------ | --------------------- | -------------------------------------------------------------------------------- |
| init() | ISmsGateway | Returns this (facade). |
| quickSend(params, callback?) | Promise<SendResult> | Single recipient. Normalizes PascalCase or camelCase params. |
| send(params, callback?) | Promise<SendResult> | Multiple recipients (To array). Supported on nest, hubtel, and route. |
| sendPersonalized(params, callback?) | Promise<SendResult> | Personalised bulk send (Destinations with per-recipient Values). Supported on nest and hubtel; route throws. |
| getGateway() | ISmsGateway | Underlying adapter (e.g. nest getBalance(), hubtel getMessageStatus()). |
Changelog
1.6.0
- New: Built-in
hubtelREST client (HubtelSmsGateway) replaceshubtel-sms-extended. Uses officialhttps://sms.hubtel.comAPI with keep-alive pooling, timeouts, and retries. - New:
send()andsendPersonalized()onhubtelvia Hubtel batch APIs.sendPersonalized()expands{$variable}placeholders client-side before sending. - New:
sendPersonalized()for personalised bulk SMS onnest(SMSOnlineGH template variables + per-destinationvalues).routethrows — usesend()orquickSend()instead. - New:
send()for multi-destination SMS onnest(SMSOnlineGHdestinationsarray),hubtel(batch simple), androute(Route Mobilenumber[]).Tois(string | number)[]. - Performance (
nest): TheNestSmsGatewaynow uses a persistent keep-alive connection pool (https.Agent) instead of opening a fresh TCP + TLS connection on every request. Subsequent sends to the same host reuse warm sockets, eliminating the per-call handshake overhead (~100–300 ms per request). - Reliability (
nest): Stale-socket errors (ECONNRESET,ECONNABORTED,EPIPE,ETIMEDOUT) that can occur when a pooled socket is reused after the server has closed it are automatically retried on a fresh connection. The default retry count is1; configure viaparam.retries. - Timeout support (
nest): Requests that stall mid-flight are now aborted after a configurable deadline (param.timeout, default10 000 ms) instead of hanging indefinitely. - New
paramfields (nest):keepAlive(defaulttrue),timeout(default10000),maxSockets(default10),retries(default1). All are optional and fully backwards-compatible; existinginit()calls require no changes. - Resource cleanup (
nest):NestSmsGatewayexposes adestroy()method (accessible viagetGateway().destroy()) that releases pooled sockets so Node does not hold the event loop open after the gateway is no longer needed. - Internal (
nest): Response chunks are now accumulated asBuffer[]and concatenated once at the end, avoiding repeated string re-allocation per chunk. The POST body is serialised to aBufferupfront soContent-LengthreadsBuffer.length(O(1)) rather than rescanning the string.
1.5.2
- Build: TypeScript
rootDiris now./srcwithinclude: ["src/**/*.ts"](scripts stayts-node-only). Previously the compiler also picked upscripts/test-live.ts, inferred a project root abovesrc/, and emitted library code underdist/src/lib/…while published entrypoints loaddist/lib/…— leaving stale or missingdist/lib/nest-gateway.js(wrongrequestBodyshape). The publisheddist/layout now matchespackage.jsonmainand always rebuilds gateway files from current sources.
1.5.1
- Fix (
nest/ all gateways):quickSendnow accepts camelCase (from,to,content,type) as well as PascalCase (From,To,Content,Type). Passing only camelCase previously leftContentundefined, so the nest JSON body omittedtextand the API returned handshake 1305 (missing or invalid message body). Validation errors throw clear messages when body or sender is empty after trim.
1.5.0
- Fix (
nest):quickSendnow reliably works in long-running processes (servers, workers). Node's global HTTP agent reuses keep-alive sockets across calls; when the provider closes an idle socket server-side, the nextquickSendthat writes a request body receivedwrite ECONNABORTEDwhilegetBalance(no body) appeared to work fine. Fixed by settingagent: falseon each request so every call opens a fresh connection rather than reusing a potentially stale one from the pool.
