mojawave
v1.0.0
Published
Official Node.js SDK for the MojaWave API — SMS and transactional messaging for Tanzania.
Maintainers
Readme
MojaWave Node.js SDK
A thin, typed Node.js/TypeScript client for the MojaWave REST API — send SMS (single, bulk, OTP), check credit balances, and verify webhooks across Tanzania's telco networks (Vodacom, Tigo, Airtel, Halotel).
npm install mojawaveNode 18+ (uses the built-in fetch). Ships ESM, CommonJS, and TypeScript types.
Zero runtime dependencies.
Quickstart
import { MojaWave } from "mojawave";
const client = new MojaWave({ apiKey: "sk_live_mw_..." }); // or MOJAWAVE_API_KEY
const msg = await client.sms.send({
to: "+255753276939",
sender: "MojaWave",
message: "Hello from Mojawave! Your verification code is 1234.",
});
console.log(msg.id, msg.status);CommonJS works too:
const { MojaWave } = require("mojawave");The client reads MOJAWAVE_API_KEY from the environment when apiKey is
omitted. Use an sk_test_mw_ key for the sandbox — no real messages, no charges.
Never expose your live API key in client-side code. Use server-side requests and environment variables only.
Sending SMS
Single message
const msg = await client.sms.send({
to: "+255712345678",
sender: "MojaWave", // sender ID (≤11 alphanumeric chars); sent as `from`
message: "Your code is 1234.",
webhookUrl: "https://example.com/webhooks/sms", // optional delivery receipts
scheduleAt: "2026-07-01T09:00:00Z", // optional ISO-8601 schedule
metadata: { customerId: "cust98765" }, // optional, echoed back
tags: ["onboarding", "verification"], // optional
});Look up a message
const msg = await client.sms.get("89b82624-f1a2-4f5e-85b5-102e79a06779");
if (msg.status === "delivered") console.log("Delivered at", msg.timeline.delivered_at);
else if (msg.status === "failed") console.log("Failed:", msg.failure_reason);Bulk send (up to 10,000 recipients)
Bulk jobs run asynchronously — you get a job back immediately, then poll it.
const job = await client.sms.bulk({
name: "Marketing Campaign Q1",
sender: "MojaWave",
message: "Hello {name}, your code is {code}",
recipients: [
{ to: "+255712345678", personalization: { name: "John", code: "ABC123" } },
{ to: "+255712345679", personalization: { name: "Jane", code: "XYZ789" } },
"+255712345680", // a bare string works too (no personalization)
],
webhookUrl: "https://example.com/webhooks",
});
const updated = await client.sms.getBulk(job.id);
console.log(`${updated.progress_percent}% — ${updated.sent_count} sent`);Unicode messages have a 70-character per-segment limit (vs. 160 for plain SMS). Plan message length accordingly.
Credits
const balances = await client.credits.balance();
console.log(balances.sms?.balance, balances.sms?.is_low_balance);
console.log(balances.email?.balance);Webhooks
MojaWave signs every webhook with an X-MojaWave-Signature header (HMAC-SHA256
of the raw body). Always verify against the raw request bytes — parsing to
JSON first can change whitespace and break the check.
import { constructEvent, WebhookVerificationError, SIGNATURE_HEADER } from "mojawave";
// Express, with express.raw({ type: "application/json" })
try {
const event = constructEvent(req.body, req.header(SIGNATURE_HEADER), WEBHOOK_SECRET);
if (event.type === "message.delivered") {
// ...
}
} catch (err) {
if (err instanceof WebhookVerificationError) return res.sendStatus(403);
throw err;
}Event types: message.sent, message.delivered, message.failed,
credits.low. See examples/express_webhook.mjs for a full handler. If you only
need a boolean, use verifySignature(payload, signature, secret).
Error handling
Every documented HTTP status maps to a typed error. All extend MojaWaveError.
| Class | HTTP | Code |
|---|---|---|
| InvalidRequestError | 400 | invalid_request |
| AuthenticationError | 401 | unauthorized |
| InsufficientBalanceError | 402 | insufficient_balance |
| UnprocessableError | 422 | unprocessable |
| RateLimitError | 429 | rate_limit_exceeded |
| ServerError | 5xx | server_error |
| APIConnectionError / APITimeoutError | — | transport failures |
import { InsufficientBalanceError, RateLimitError } from "mojawave";
try {
await client.sms.send({ to: "+255712345678", message: "hi" });
} catch (err) {
if (err instanceof InsufficientBalanceError) {
// top up
} else if (err instanceof RateLimitError) {
await new Promise((r) => setTimeout(r, (err.retryAfter ?? 1) * 1000));
} else {
throw err;
}
}The client automatically retries 429 and 5xx responses with exponential
backoff (honouring Retry-After), controlled by maxRetries (default 2).
Configuration
const client = new MojaWave({
apiKey: "sk_live_mw_...",
environment: "live", // or "sandbox"
timeout: 30_000, // milliseconds
maxRetries: 2,
baseUrl: "https://api.mojawave.com/v1",
fetch: customFetch, // optional: inject your own fetch
});Rate-limit headers from the most recent response are on client.rateLimit
(.limit, .remaining, .reset).
Development
npm install
npm run build # tsup -> ESM + CJS + .d.ts in dist/
npm test # builds, then runs the node:test suite
npm run typecheck # tsc --noEmitLicense
MIT
