Skip to Content
SDKsTypeScript

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 sendafrica

For 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 })
ConfigTypeDefaultDescription
apiKeystring—Required. Sent as X-API-Key and Authorization: Bearer.
baseUrlstringhttps://api.sendafrica.online/v1Override for sandbox or self-hosted APIs.
timeoutMsnumber15000Aborts requests after this many ms.
maxRetriesnumber2Retries 429/5xx and network errors.
fetchfunctionglobalThis.fetchInjectable 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
HelperDescription
normalizeTzPhone(input)Converts 0712345678, 255..., +255... to E.164 +255...; throws InvalidPhoneNumberError otherwise.
isValidTzPhone(input)Returns boolean.
TZ_MOBILE_PREFIXESAllowed 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) } }
ErrorThrownProperties
SendAfricaErrorAny API error (success: false)code, message, requestId, httpStatus; helpers isInsufficientCredits, isRateLimited, isUnauthorized
SendAfricaNetworkErrorTimeouts, DNS/connection failures, non-JSON responsescause (original error)
InvalidPhoneNumberErrorPhone helpers on an invalid Tanzania numbermessage

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 / MethodEndpointReturns
sms.send(params, options?)POST /smsSendSmsResult
sms.sendMany(messages, options?)Loops POST /smsBulkSMSResult
sms.bulk({to, message, from?})POST /sms/bulkBulkSendResult
sms.logs(query?)GET /sms/logsMessageLogListResponse
sms.logsJwt(jwtToken, query?)GET /sms/logs (JWT)MessageLogListResponse
credits.balance()GET /credits/balanceCreditBalance
credits.history(query?)GET /credits/historyCreditTransaction[]
rates.list()GET /ratesRateInfo[]
rates.get(country)GET /rates/{country}RateInfo
senderIds.requirements()GET /sender-ids/requirementsSenderIDRequirements
senderIds.usable(provider?)GET /sender-ids/usableUsableSenderID[]
senderIds.list()GET /sender-idsSenderID[]
senderIds.create(params)POST /sender-idsSenderID
senderIds.get(senderId)GET /sender-ids/{id}SenderID
senderIds.setDefault(senderId)PUT /sender-ids/defaultSenderID
payments.rate()GET /vouchers/rateVoucherRate
payments.create(params, options?)POST /vouchersVoucherResult
payments.sendDeclaredPhoneOtp(phone)POST /vouchers/otp/sendDeclaredPhoneOtpResult
payments.verifyDeclaredPhoneOtp(phone, otp)POST /vouchers/otp/verifyDeclaredPhoneOtpResult

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.

Last updated on