UPESI SMS API
Send SMS from your own website, app or business system. The API is REST with JSON. Base URL:
Building with an AI assistant? Give it /ai.md: one file with every rule, endpoint and code sample it needs.
Your key, test connection, send, status. Start here. I have a system and want to sell SMS (reseller)
Customer accounts, credits, sending for them. Apply
Phone numbers: international format without "+" (255754123456). Tanzanian numbers written as 0754 123 456 are also accepted. Times are ISO 8601.
Authentication
Create a key in the dashboard under API & developers → API keys. Copy it when it is created. Forgot it? Press Show next to the key and type your password. We check keys by their hash; the copy for Show is stored encrypted. Send it on every request:
Keep the key on your server. Never put it in a web page or mobile app. You can limit a key to your server's IP addresses, restrict its permissions, revoke it or regenerate it at any time.
The key is all you need. Send with it, and ask for delivery status with it. Webhooks are optional.
Test connection
Put this behind your system's Save and test button. Any valid key works; a wrong or revoked key answers 401 invalid_token.
Quick start
Send SMS
| Field | Required | Description |
|---|---|---|
| to | yes | One number, or an array of up to 1,000 numbers. |
| message | yes | Text. Up to 5 SMS parts: 765 characters (160 in one SMS, 153 per SMS when longer), or 335 with an emoji or other special character (70, then 67 per SMS). {name}-style variables are replaced from vars. |
| sender_id | no | An approved sender ID of your account. Default: your default sender ID. |
| type | no | transactional (default) or promotional. Promotional SMS outside 06:00–21:00 Tanzania time are scheduled for the morning. |
| schedule_at | no | ISO time to send later. |
| reference | no | Your own unique id (or the Idempotency-Key header). Sending the same reference again returns the first result and is not charged twice. |
| vars | no | Object of variables, e.g. {"name":"Asha"}. |
Response 201:
Bulk SMS
Same text to many people (with variables), up to 100,000 recipients per call:
Or different texts, up to 1,000 per call: {"messages":[{"to":"…","message":"…"}]}. Each item gets its own result.
A new account's first bulk send to 50 or more people is checked by UPESI SMS before it goes out (status awaiting_approval); later sends go out at once.
Delivery status
| Status | Meaning |
|---|---|
| queued / scheduled | Waiting to go out. |
| submitted / sent | Accepted by the network, waiting for the phone. |
| delivered | Reached the phone. |
| failed / rejected / expired | Not delivered. error says why. |
| dnd | The number blocks promotional SMS. |
| unknown | The network did not answer yet; we keep asking. It is never resent. |
Status of many (no webhook needed)
Up to 100 message ids per call, or every message sent with one reference. Ask every few minutes until each is final (delivered, failed, rejected, expired, dnd). We keep asking the network for up to 3 days.
History
Balance
Sender IDs
Your sender IDs with their status. Only approved ones can be used. Request new ones in the dashboard (documents are needed).
OTP verification
We create the code, send it and check it. You never store codes.
Optional message with {code} to use your own words. At most 5 codes per number per 10 minutes. A code is never delivered after it expires: if it has not left our queue by then it is not sent (status expired, credit returned), and the network is told to stop trying at the same moment.
Reasons: wrong_code, expired, too_many_attempts (5), already_used.
Events (automations)
Create an automation in the dashboard (trigger "When my system sends an event"), then report the event; the matching message is sent.
Resellers: sell UPESI SMS on your own platform
For billing platforms (WiFi billing, school, SACCO or POS systems) whose own customers want SMS under their own names. Each of your customers gets a sub-account; you give them credits from your balance and charge them your own price on your system. We never bill your customers.
- Ask UPESI SMS to make your account a reseller. You get a wholesale price, and your API keys get the
resellerpermission. - Buy credits (dashboard → Wallet, or bank top-up).
- Open a sub-account for each customer, with
external_ref= your own id for them. - Request each customer's sender ID with their documents. We check it and get it approved by the networks (1–3 working days).
- When a customer pays you, move credits to them. Then send for them with the normal endpoints plus the header
X-Account-Id.
The same is available on the website under My customers for resellers who do not want to code it.
Customers (sub-accounts)
Safe to repeat: the same external_ref returns the existing customer with "created": false (200).
A suspended customer cannot send. Wherever an {id} is asked, our account_id or your external_ref both work.
Moving credits
amount_tzs (optional) is what your customer paid you; it only feeds your earnings report (GET /v1/reseller/earnings). A negative number takes unused credits back. reference must be unique for each move: repeating it moves nothing ("duplicate": true), so a retry after a timeout is safe. Not enough credits: 402 insufficient_credits.
Sending for a customer
Every endpoint above (send, bulk, status, history, balance, sender IDs, OTP, webhooks) works for one of your customers when you add:
The customer's credits and sender IDs are used. A customer may also use your own approved sender IDs. Their delivery reports reach your webhooks with account_id and external_ref.
Customer sender IDs
Your customers' requests come to you first. A new request waits as with_reseller until you submit it to UPESI SMS (on your dashboard under My customers → Sender ID requests, or with the calls below). If your system already checked the name, send "submit": true and it comes straight to us.
All your customers' requests (any status), with account_id, external_ref and reject_reason.
Removes a request still waiting with you (never one already with us).
At most 11 characters, spaces included ('Upesi Lipa' = 10), kept exactly as typed. No documents are needed (optional: PDF, PNG or JPG, up to 4 MB each). Names of brands the customer does not own are refused.
Status: with_reseller (waiting for you) → pending (we are checking) → submitted (with the networks) → approved or rejected (with reject_reason). After a rejection, ask for another name the same way. Webhook events sender_id.approved and sender_id.rejected tell your system the moment it is decided.
Usage and balance
Use it to bill your customers or to check your own invoices. Default period: this month.
Today, yesterday, this week, this month, last month and all time: SMS bought from us and what you paid, SMS given to your customers, money collected (from amount_tzs, or your usual selling price), your cost and profit, SMS your customers sent.
Webhooks (optional)
Not required: ask for status with your key instead. Use a webhook only if you want reports the second they arrive. Add a URL under API & developers → Webhooks (or POST /v1/webhooks {"url","events"}). Events: sms.submitted, sms.delivered, sms.failed, sms.incoming, otp.verified, sender_id.approved, sender_id.rejected.
Other events: sms.incoming → {"from","text","keyword","received_at"}; otp.verified → {"otp_id","to"}. For a reseller, events of its customers also arrive on the reseller's webhooks, with account_id and external_ref added to data.
Check the signature: HMAC_SHA256(secret, timestamp + "." + raw_body), hex, prefixed sha256=. Reject old timestamps. Answer with any 2xx within 10 seconds; otherwise we retry after 1, 5, 30, 120 and 360 minutes.
Errors
| HTTP | code | When |
|---|---|---|
| 401 | missing_token, invalid_token | No key, wrong key, revoked or expired key. |
| 402 | insufficient_credits | Not enough credits. Nothing was sent or charged. |
| 403 | permission_denied, ip_not_allowed, account_suspended, reseller_suspended, not_reseller | The key may not do this, the request came from another IP, or the account (or its reseller) is suspended. |
| 404 | not_found, account_not_found | Unknown message id, endpoint, or customer under your reseller account. |
| 422 | sender_not_approved, empty_message, no_recipients, nothing_to_send, too_long, too_many, invalid_phone, invalid_credits, invalid_sender, documents_required | The request cannot be sent as it is. |
| 429 | rate_limited, too_many_otps | Too fast. Wait and retry. |
| 500 | server_error | Our side. The X-Request-Id header helps support find it. |
Rate limits
10 requests per second per account by default (ask for more). Bulk endpoints count as one request. Messages leave our queue at the speed the networks allow; large sends finish in minutes.
Billing and SMS parts
Credits are SMS parts. Plain text (Kiswahili, English): 160 characters in one part, 153 per part when longer. One emoji or other special character makes the whole message Unicode: 70 characters in one part, 67 per part. Credits are taken when you send; a message the network refuses before sending is refunded automatically. Every call is logged under API logs.
Code: Node.js, Python, PHP
Read the key from an environment variable on your server. Always pass a reference so a retry never sends twice.
Retry only on timeouts, 429 and 5xx, with the same reference. Never retry other 4xx in a loop.
Building with AI
Using Claude, ChatGPT, Cursor or Copilot? Give it /ai.md and tell it what you want, for example "Add SMS receipts to my billing system using UPESI SMS". The file has every rule the integration must follow (key on the server, retry-safe references, sender IDs, webhook signatures, resellers) and ready code in Node.js, Python and PHP. AI tools that look for /llms.txt find it on their own.
Let your users connect from the dashboard
Your users should not edit a server file to turn SMS on. Build a Settings → SMS (UPESI SMS) page: the owner pastes the API key, presses Save and test (your server calls GET /v1/me), picks the Sender ID and sends a test SMS to their phone. The key is saved on your server, encrypted (the encryption secret stays in .env), and shown only as usms_live_••••1a2b. Full checklist in /ai.md, section 2a. To have an AI build it, paste: