unismsgateway
v1.5.3
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. |
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) |
| hubtel | clientId, clientSecret | debug |
| 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 |
| 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
}
};
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. |
**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 call quickSend() and send a real SMS. If unset or not true, only init and balance checks run. |
| TEST_FROM | Yes when TEST_SEND=true | QuickSendParams.From. |
| TEST_TO | Yes when TEST_SEND=true | QuickSendParams.To. |
| TEST_CONTENT | No | Message body; if omitted, the script uses a built-in default string. |
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 (routeSms,HubtelSms, 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 is 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) | hubtel-sms-extended |
| 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.
No host / protocol in IgatewayParam for Hubtel in this library; configuration follows hubtel-sms-extended.
const gateway = unisms.init({
platformId: 'hubtel',
param: {
clientId: 'your-client-id',
clientSecret: 'your-client-secret'
}
});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). |
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 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
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 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);
}
);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.
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). |
**smsPlatform instance methods**
| Method | Returns | Description |
| ------------------------------ | --------------------- | -------------------------------------------------------------------------------- |
| init() | ISmsGateway | Returns this (facade). |
| quickSend(params, callback?) | Promise<SendResult> | Normalizes PascalCase or camelCase params, then delegates to the active gateway. |
| getGateway() | ISmsGateway | Underlying adapter (for nest: getBalance()). |
Changelog
1.6.0
- 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.
