--- name: hudumasms description: Send SMS in Tanzania through the HudumaSMS API. Use when a task involves sending SMS, OTPs, or bulk messages to Tanzanian numbers, registering a Sender ID, checking delivery status, or estimating SMS cost in credits. --- # Integrating with HudumaSMS Bulk SMS for Tanzania. Prepaid: credits are bought in advance and each message **part** consumes one. Base URL: `https://api.huduma.cloud` Docs: https://docs.hudumasms.com · Dashboard: https://sms.huduma.cloud ## Authenticate Exchange dashboard credentials for a token, then send it as a bearer credential. ```bash curl -X POST https://api.huduma.cloud/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"you@example.com","password":"your-password"}' ``` ``` Authorization: Bearer ``` An older `X-Huduma: Bearer ` header is still accepted, so existing integrations keep working. Use `Authorization` for anything new. **The token spends money.** It carries full account permissions including the ability to send billed messages. Keep it server-side. Never put it in a mobile app, a browser bundle, or a repository — write it to an environment variable and read it from there. ## Send a message ```bash curl -X POST https://api.huduma.cloud/api/v3/outgoings/send \ -H "Authorization: Bearer $HUDUMASMS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "senderId": "MYBRAND", "envelopes": [ { "number": "255766298542", "message": "Karibu!", "thirdPartyRef": "order-1187" } ] }' ``` One entry in `envelopes` per recipient, **each carrying its own text**, so one call can send different messages to different people. | Field | Required | Notes | | --- | --- | --- | | `senderId` | yes | An approved Sender ID on your account. Max 11 characters. | | `envelopes[].number` | yes | International format without `+`, e.g. `255766298542`. | | `envelopes[].message` | yes | The text. | | `envelopes[].thirdPartyRef` | no | Your own reference, echoed back. Use it to reconcile. | Numbers are normalised, so `0766298542` and `+255766298542` are both accepted. Only Tanzanian mobile numbers work. ## The response envelope Every response, success or failure, has the same shape: ```json { "status": "202 ACCEPTED", "code": 202, "message": "...", "timestamp": "2026-09-23T10:14:22Z", "reqId": "REQ7F2A9C41", "path": "/api/v3/outgoings/send", "data": { "balance": 4820, "outgoings": [ ... ] } } ``` On failure `data` is omitted and `errors` carries the validation messages. **Log `reqId`.** It is how support locates a specific request, and without it a report of "a message did not arrive" is unanswerable. Read `data.balance` after each send — it is the remaining credit, and the cheapest way to notice you are running out before sending stops working. ## Four things that cost real money Get these wrong and the bill, not an error, tells you. **Each envelope is charged separately.** Sending identical text to 500 people costs at least 500 credits. There is no shared-body discount. Budget per recipient, not per request. **One non-GSM character re-prices the whole message.** Standard GSM text allows 160 characters per part. A single accented character, emoji, curly quote or en-dash drops the *entire* message to 70 per part. | Content | Characters per part | | --- | --- | | Standard GSM | 160 | | Any non-GSM character anywhere | 70 | A 140-character message is 1 credit. Add one accented letter and it becomes 3 parts — 3 credits — for the same text. This catches people writing Swahili with accents. If you are generating message text, prefer plain ASCII and check the length before sending. **`callbackUrl` is accepted and ignored.** It exists for compatibility with the older platform; nothing is ever posted to it. Delivery is obtained by polling. Do not build a flow that waits on a callback — it will wait forever. **`202` means queued, not delivered.** It says the network accepted the message, nothing more. ## Delivery status | Status | Meaning | | --- | --- | | `SENT` | Handed to the network. | | `DELIVERED` | The network confirmed it reached the handset. | | `FAILED` | The network rejected it; `failureReason` says why. | HudumaSMS polls the network for delivery reports, so a message reading `SENT` immediately after your request may read `DELIVERED` or `FAILED` shortly after. Re-read the message from your sending history rather than inferring delivery from the send response. ## Sender IDs The name recipients see instead of a number. Register with `POST /sender-ids`; it needs network approval before it can be used, which is not instant. Until one is approved, sends must use an already-approved name. A Sender ID left unused for a long period is withdrawn by the networks, so an account that registers one and then sends nothing may find it gone later. ## Errors | Code | Meaning | What to do | | --- | --- | --- | | `400` | Validation failed | Read `errors`. Usually a missing field or a malformed number. | | `401` | Not authenticated | Token missing, malformed or expired. Get a new one. | | `402` | Insufficient credits | Top up. **Nothing was sent.** | | `403` | Not permitted | The account lacks permission for this endpoint. | | `404` | Not found | Check the path and any id in it. | | `429` | Too many requests | Back off and retry more slowly. | | `500` | Server error | Safe to retry after a short delay; contact support with `reqId` if it persists. | Retry `429` and `500` with backoff. Do **not** blindly retry a `400` or `402` — the first will fail identically forever, and the second means you have no credit rather than that the request was wrong. Be careful retrying a send whose response you never saw: the message may already have gone. Use `thirdPartyRef` and check your history before resending, or the recipient gets two copies and you pay twice. ## Writing an integration - Read the token from the environment, never a literal. - Send in batches of envelopes rather than one request per recipient — it is the same cost and far fewer round trips. - Validate and normalise numbers before sending, so a bad number is a local error rather than a rejected envelope you have to reconcile. - Store the returned message id and your `thirdPartyRef` together, so delivery can be reconciled later without guessing from timestamps. - Check `data.balance` and warn well before it reaches zero; sending simply stops at `402`. ## Full documentation https://docs.hudumasms.com — and https://docs.hudumasms.com/llms.txt for a machine-readable index of every page.