TypeScript SDK
The official TypeScript/JavaScript client for the SendAfrica REST API. It is fully typed, has zero runtime dependencies, and uses the native fetch API with automatic retries, backoff, and error mapping.
- Package:
sendafrica— Node.js 18 or newer - Zero runtime dependencies; uses global
fetch - CJS (
require) and ESM (import) builds with bundled type declarations - Source:
github.com/SendAfrica/Typescript-SDK
Install
npm install sendafricaFor the full SDK guide, see the official docs at sdk.sendafrica.online/sdks/typescript .
Quickstart
import { SendAfricaClient } from 'sendafrica'
const client = new SendAfricaClient({
apiKey: process.env.SENDAFRICA_API_KEY!,
})
const result = await client.sms.send({
to: '0712345678',
message: 'Your verification code is 4921.',
})
console.log(result.messageId, result.status, result.creditsUsed)Instantiate the client in a server-side route, worker, or backend service — never in a browser bundle, because that exposes your API key.
Client setup
import { SendAfricaClient } from 'sendafrica'
const client = new SendAfricaClient({
apiKey: 'SA-...', // required
baseUrl: 'https://api.sendafrica.online/v1', // optional override
timeoutMs: 15_000, // optional, default 15000
maxRetries: 2, // optional, default 2
fetch: customFetch, // optional: supply your own fetch
})| Config | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Required. Sent as X-API-Key and Authorization: Bearer. |
baseUrl | string | https://api.sendafrica.online/v1 | Override for sandbox or self-hosted APIs. |
timeoutMs | number | 15000 | Aborts requests after this many ms. |
maxRetries | number | 2 | Retries 429/5xx and network errors. |
fetch | function | globalThis.fetch | Injectable fetch (needed on Node < 18). |
On Node < 18 pass a fetch implementation (e.g. from node-fetch); otherwise the constructor throws.
Sending SMS
Single message
Phone numbers are normalized to E.164 by default; pass skipPhoneNormalization: true to keep your format as-is:
const result = await client.sms.send(
{
to: '0712345678',
message: 'Welcome to SendAfrica!',
from: 'SENDAFRICA', // optional registered sender ID
},
{
idempotencyKey: 'order-123', // optional safe retries
}
)
console.log(result.messageId, result.creditsUsed, result.requestId)Returns a typed SendSmsResult: messageId, status, cost, creditsUsed, requestId, timestamp.
Client-side send many
const res = await client.sms.sendMany(
[
{ to: '0712345678', message: 'Hi Ali' },
{ to: '0765432109', message: 'Hi Zainab', idempotencyKey: 'm2' },
],
{ rateLimitPerSec: 10.0 } // default 10
)
console.log(res.results.length, res.failed)Per-message failures are collected in res.failed ({index, to, error}) instead of aborting the batch.
Server-side bulk SMS
One message to many recipients in a single API call. The SDK caps at 100 recipients; the server reports and refunds partial failures per recipient.
const bulk = await client.sms.bulk({
to: ['0712345678', '0765432109'],
message: 'Black Friday starts now!',
from: 'MYBRAND',
})
console.log(bulk.total, bulk.sent, bulk.failed)
for (const r of bulk.results) {
console.log(r.to, r.status, r.messageId, r.error)
}Message logs
const logs = await client.sms.logs({
page: 1,
perPage: 25,
status: 'delivered', // pending | sent | delivered | failed
search: '0712345678',
dateFrom: '2026-09-01T00:00:00Z',
})
for (const entry of logs.items) {
console.log(entry.toPhone, entry.status, entry.creditsUsed, entry.deliveredAt)
}With a dashboard JWT (instead of the API key):
const logs = await client.sms.logsJwt(jwtToken, { page: 1, perPage: 25, status: 'sent' })Credits
const balance = await client.credits.balance()
console.log(balance.accountId, balance.balance)
const history = await client.credits.history({ page: 1, perPage: 25 })
for (const txn of history) {
console.log(txn.type, txn.amount, txn.balanceAfter, txn.description)
}Rates
The rate card is public.
const rates = await client.rates.list()
for (const r of rates) console.log(r.name, r.iso2, r.rateTzs)
const tz = await client.rates.get('TZ')
console.log(tz.name, tz.rateTzs)Sender IDs
const reqs = await client.senderIds.requirements()
console.log(reqs.eligibility?.canRequest, reqs.purposes)
const usable = await client.senderIds.usable('swala')
const all = await client.senderIds.list()
const created = await client.senderIds.create({
name: 'MYBRAND',
country: 'TZ',
purpose: 'Marketing',
sampleMessage: 'Welcome to MYBRAND',
documents: [{ requirementUid: '...', filename: 'doc.pdf', contentBase64: '...' }],
})
const one = await client.senderIds.get(created.id)
await client.senderIds.setDefault(created.id)Payments (vouchers)
const rate = await client.payments.rate()
console.log(rate.minAmountTzs, rate.tiers)
const payment = await client.payments.create(
{
provider: 'snippe', // or 'manual'
amount: 10000, // TZS — credits computed server-side
phone: '0712345678',
},
{ idempotencyKey: 'topup-42' }
)
console.log(payment.id, payment.status, payment.creditAmount)Declared-payer OTP flow:
await client.payments.sendDeclaredPhoneOtp('0765432109')
await client.payments.verifyDeclaredPhoneOtp('0765432109', '4921')Webhooks
Verify and parse the sms.delivered/sms.failed events. The signature is the HMAC-SHA256 hex digest of the raw body in the X-SendAfrica-Signature header — verify before parsing JSON:
import crypto from 'node:crypto'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const rawBody = await request.text()
const expected = crypto
.createHmac('sha256', process.env.SENDAFRICA_WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex')
const actual = request.headers.get('x-sendafrica-signature') ?? ''
if (
actual.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(actual))
) {
return NextResponse.json({ error: 'invalid signature' }, { status: 401 })
}
const event = JSON.parse(rawBody)
console.log(event.event, event.message_id, event.to, event.status)
return NextResponse.json({ ok: true }) // durable work before this return
}See the webhooks reference for the full contract and retry semantics.
Utilities
Top-level helper functions and constants:
import {
normalizeTzPhone,
isValidTzPhone,
TZ_MOBILE_PREFIXES,
detectEncoding,
getSmsPartInfo,
countSmsParts,
} from 'sendafrica'
normalizeTzPhone('0712345678') // '+255712345678'
isValidTzPhone('0712345678') // true
getSmsPartInfo('Hello world') // { encoding: 'GSM-7', length: 11, parts: 1, creditsRequired: 1 }
countSmsParts('Long message...') // number of parts| Helper | Description |
|---|---|
normalizeTzPhone(input) | Converts 0712345678, 255..., +255... to E.164 +255...; throws InvalidPhoneNumberError otherwise. |
isValidTzPhone(input) | Returns boolean. |
TZ_MOBILE_PREFIXES | Allowed Tanzania mobile prefixes ('071'–'078', '068', '069'). |
detectEncoding(message) | Returns 'GSM-7' or 'UCS-2'. |
getSmsPartInfo(message) | Returns { encoding, length, parts, creditsRequired }. |
countSmsParts(message) | Parts count shorthand. |
Error handling
import {
SendAfricaError,
SendAfricaNetworkError,
} from 'sendafrica'
try {
await client.sms.send({ to: '999', message: 'test' })
} catch (err) {
if (err instanceof SendAfricaError) {
console.log(err.code, err.message, err.requestId, err.httpStatus)
if (err.isInsufficientCredits) { /* 402 */ }
if (err.isRateLimited) { /* 429 */ }
if (err.isUnauthorized) { /* 401 */ }
} else if (err instanceof SendAfricaNetworkError) {
console.error('network failure or timeout', err.cause)
}
}| Error | Thrown | Properties |
|---|---|---|
SendAfricaError | Any API error (success: false) | code, message, requestId, httpStatus; helpers isInsufficientCredits, isRateLimited, isUnauthorized |
SendAfricaNetworkError | Timeouts, DNS/connection failures, non-JSON responses | cause (original error) |
InvalidPhoneNumberError | Phone helpers on an invalid Tanzania number | message |
Retries and backoff
The client retries 429 (honoring Retry-After) and 5xx responses, plus network failures, up to maxRetries times (default 2). Backoff starts at 1 second, doubles to a maximum of 8 seconds, and adds up to 250 ms of jitter. Each request carries a fresh X-Request-Id, and the default timeout is 15 seconds.
Reference
| Resource / Method | Endpoint | Returns |
|---|---|---|
sms.send(params, options?) | POST /sms | SendSmsResult |
sms.sendMany(messages, options?) | Loops POST /sms | BulkSMSResult |
sms.bulk({to, message, from?}) | POST /sms/bulk | BulkSendResult |
sms.logs(query?) | GET /sms/logs | MessageLogListResponse |
sms.logsJwt(jwtToken, query?) | GET /sms/logs (JWT) | MessageLogListResponse |
credits.balance() | GET /credits/balance | CreditBalance |
credits.history(query?) | GET /credits/history | CreditTransaction[] |
rates.list() | GET /rates | RateInfo[] |
rates.get(country) | GET /rates/{country} | RateInfo |
senderIds.requirements() | GET /sender-ids/requirements | SenderIDRequirements |
senderIds.usable(provider?) | GET /sender-ids/usable | UsableSenderID[] |
senderIds.list() | GET /sender-ids | SenderID[] |
senderIds.create(params) | POST /sender-ids | SenderID |
senderIds.get(senderId) | GET /sender-ids/{id} | SenderID |
senderIds.setDefault(senderId) | PUT /sender-ids/default | SenderID |
payments.rate() | GET /vouchers/rate | VoucherRate |
payments.create(params, options?) | POST /vouchers | VoucherResult |
payments.sendDeclaredPhoneOtp(phone) | POST /vouchers/otp/send | DeclaredPhoneOtpResult |
payments.verifyDeclaredPhoneOtp(phone, otp) | POST /vouchers/otp/verify | DeclaredPhoneOtpResult |
Prefer camelCase types (messageId, creditsUsed, toPhone). The Model.fromDict factories attached to the exports map snake_case API responses into these shapes. See the webhooks and SMS API references for the underlying REST contract.