dialog-esms
v0.1.0
Published
Unofficial TypeScript/JavaScript SDK for the Dialog eSMS API (send SMS, check campaign status, delivery reports, balance).
Maintainers
Readme
dialog-esms
Unofficial TypeScript/JavaScript SDK for the Dialog eSMS API — send SMS, check campaign status, handle delivery reports, and query balance without hand-rolling HTTP, auth-token lifecycle, or error-code decoding.
- 🔑 Automatic token management — logs in, caches the token, refreshes on expiry.
- 📨 Both send paths — Bearer-token
POSTandesmsqkURL/GET. - 🧠 Friendly inputs — pass
to: "0771234567"or an array; numbers are normalized. - 🎯 Typed throw-only errors — every failure is an
EsmsErrorwith a decoded code. - 🪶 Zero runtime dependencies — uses the global
fetch(Node ≥ 18, Bun, Deno, edge). - 📦 Dual ESM + CJS with full type declarations.
Disclaimer: This is a community SDK and is not affiliated with, endorsed by, or supported by Dialog Axiata or Adeona Technologies. "eSMS" and "Dialog" belong to their respective owners. Behavior is based on the eSMS API Document v2.9.
Installation
npm install dialog-esmsRequires Node.js ≥ 18 (for global fetch), or any runtime that provides fetch.
Quick start
import { EsmsClient } from "dialog-esms";
const esms = new EsmsClient({
username: "947xxxxxxxx",
password: "your-password",
});
const res = await esms.sendSms({
to: ["0771234567", "94777888665"],
message: "Your OTP is 123456",
sourceAddress: "MyBrand", // optional; max 11 chars. Omit to use your default mask.
});
console.log(res.campaignId, res.walletBalance);CLI: get a token quickly
The package ships a small dialog-esms command for grabbing an access token without writing
code — handy for testing, scripts, or exporting a token into your environment.
# Prompts for the password (hidden input); prints the bare token to stdout
npx dialog-esms token --username 947xxxxxxxx
# Non-interactive via env vars
ESMS_USERNAME=947xxxxxxxx ESMS_PASSWORD=secret npx dialog-esms token
# Capture into a shell variable (only the token goes to stdout; logs go to stderr)
export ESMS_TOKEN=$(ESMS_USERNAME=947xxxxxxxx ESMS_PASSWORD=secret npx dialog-esms token)
# Full details as JSON
npx dialog-esms token -u 947xxxxxxxx --jsonThe token prints to stdout so it's pipeable; all messages and errors go to stderr, and
a failed login exits non-zero. Prefer ESMS_PASSWORD or the interactive prompt over
--password (which can leak into shell history). See docs/cli.md.
Authentication modes
Construct the client one of three ways depending on what you have:
// 1. Username + password — the SDK logs in and manages the token for you.
new EsmsClient({ username, password });
// 2. A bearer token you already obtained (no auto-refresh unless you also pass creds).
new EsmsClient({ token: "eyJ..." });
// 3. A URL Message Key for the GET endpoints (sendSmsViaUrl / checkBalance).
new EsmsClient({ esmsqk: "eyJ..." });You never need to touch the token yourself — sendSms and checkTransaction acquire and
refresh it automatically. If the server reports the token expired mid-call, the SDK re-logs-in
once and retries (only when username/password were provided).
API
new EsmsClient(options)
| Option | Type | Default | Notes |
|--------|------|---------|-------|
| username | string | — | For automatic login. |
| password | string | — | For automatic login. |
| token | string | — | Pre-obtained bearer token. |
| esmsqk | string | — | URL Message Key for the GET endpoints. |
| baseUrl | string | https://esms.dialog.lk | Override the API host. |
| tokenExpirySkewMs | number | 60000 | Re-login this early before real expiry. |
| timeoutMs | number | 30000 | Per-request timeout. |
| fetch | typeof fetch | global fetch | Inject a custom implementation. |
sendSms(input) → Promise<SendSmsResult>
Send an SMS campaign (POST /api/v2/sms).
const res = await esms.sendSms({
to: "0771234567", // string | string[]
message: "Hello!",
transactionId: 1001, // optional; string | number | bigint; auto-generated if omitted
sourceAddress: "MyBrand", // optional; max 11 chars
paymentMethod: 0, // optional; 0 = wallet (default), 4 = package
pushNotificationUrl: "https://app/dr", // optional; delivery-report webhook
});
// -> { campaignId, campaignCost, walletBalance, duplicatesRemoved,
// invalidNumbers, maskBlockedNumbers, transactionId, comment, raw }Transaction ids are documented as up to 18 digits, which overflows a JS
number. Pass them as astringorbigintto stay precise. Each must be unique — reusing one throws error104. OmittransactionIdto have the SDK generate a safe unique id.
checkTransaction(transactionId) → Promise<TransactionStatusResult>
Check a campaign's status (POST /api/v2/sms/check-transaction).
const { campaignStatus } = await esms.checkTransaction(1001);
// campaignStatus: "pending" | "running" | "completed"Polling example (respect the 120 requests/minute limit — poll no faster than ~2s):
async function waitForCompletion(esms, txId, { timeoutMs = 60_000, intervalMs = 3000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const { campaignStatus } = await esms.checkTransaction(txId);
if (campaignStatus === "completed") return campaignStatus;
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error("Timed out waiting for campaign completion");
}sendSmsViaUrl(input) → Promise<SendSmsUrlResult>
Send via the URL/GET path using an esmsqk key (requires admin-enabled access).
const esms = new EsmsClient({ esmsqk: "your-key" });
await esms.sendSmsViaUrl({
to: ["0771234567", "94777888665"],
message: "Hello via URL",
sourceAddress: "MyBrand", // optional
paymentType: 0, // optional; 0 = wallet, 4 = package
});checkBalance() → Promise<BalanceResult>
Query wallet balance via the URL/GET path (requires esmsqk).
const { balance } = await esms.checkBalance();parseDeliveryReport(input) → DeliveryReport
Parse an inbound delivery-report callback. eSMS calls your push_notification_url as
GET ...?campaignId=&msisdn=&status=. Accepts a URL, query string, URLSearchParams, or an
Express-style { query } object.
import { parseDeliveryReport } from "dialog-esms";
const report = parseDeliveryReport({ url: req.url }); // or ({ query: req.query })
// -> { campaignId, msisdn, status, statusText, isSubmitted, isDelivered, isFailure }Status codes: 1 submitted to SMSC, 2 submission failed, 3 delivered, 4 delivery
failed. A successful delivery emits both 1 and 3, and order is not guaranteed — make
your handler idempotent (dedupe on campaignId + msisdn + status). See
examples/ for Express and Next.js route handlers.
Error handling
Every non-success response throws an EsmsError; transport failures throw EsmsNetworkError.
import { EsmsError, EsmsNetworkError } from "dialog-esms";
try {
await esms.sendSms({ to: "0771234567", message: "hi", transactionId: 1001 });
} catch (err) {
if (err instanceof EsmsError) {
console.error(err.code, err.codeText, err.namespace); // e.g. 104 "Transaction ID is already used" "post"
console.error(err.comment); // raw server comment, when present
} else if (err instanceof EsmsNetworkError) {
console.error("network/timeout:", err.message);
}
}The POST and GET endpoints use separate error-code namespaces (err.namespace is
"post" or "get"). Full tables: docs/error-codes.md, or the
exported POST_ERROR_CODES / GET_ERROR_CODES maps and describeError(namespace, code).
Limits & good practices
- Recommended max recipients per request: 1000 (POST), 100 (GET).
- Throughput per account: send 20 TPS, general API 30 TPS, status check 2 TPS (and ≤ 120 status checks/minute).
- Campaigns generally cannot be created during a nightly blackout window (~08:00 PM–08:00
AM) — surfaces as error
118(POST) /2013(GET). - Use a new transaction id on every retry.
See docs/ for the full, per-feature reference and spec/ for the
distilled API specification these docs are built from.
Development
npm install
npm run build # dual ESM/CJS + .d.ts via tsup
npm test # vitest
npm run typecheck # tsc --noEmitLicense
MIT
