# NewHope SMS — Complete API Reference # Version: 1.0.0 # Last updated: 2026-06-07 # Human docs: https://sms.newhope.co.tz/docs # OpenAPI JSON: https://sms.newhope.co.tz/openapi.json (import into Postman / Insomnia) # Plain text: https://sms.newhope.co.tz/llms.txt NewHope SMS is a Tanzanian bulk SMS REST API for businesses, schools, hospitals, churches, NGOs, and developers. Works with every programming language — if it can send an HTTP request, it integrates with NewHope SMS. ============================================================================= QUICK START — SEND YOUR FIRST SMS IN 5 MINUTES ============================================================================= Step 1 — Create an account and generate API keys Sign up: https://sms.newhope.co.tz/accounts/register/ Dashboard → API Keys → Generate New Key → choose a sender ID → copy both keys shown. IMPORTANT: The Secret Key (nhs_…) is shown only once. Save it immediately. Step 2 — Send a test SMS using cURL (works on Linux, macOS, Windows, Git Bash) curl -X POST https://sms.newhope.co.tz/v1/sms/send/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"recipient":"+255712345678","message":"Hello from NewHope SMS!","sender_id":"NewHope"}' Step 3 — Check the response { "id": "9f3a1c2e-…", "status": "sent", "recipient": "+255712345678", "sender_id_name": "NewHope", "gateway_message_id": "ATXid_xxxxxxxxxxxxx", "cost": "20.00", "segments": 1 } Use the "id" to check delivery status anytime. ============================================================================= OVERVIEW ============================================================================= Base URL : https://sms.newhope.co.tz/v1 Protocol : HTTPS only — plain HTTP is rejected Format : JSON (set Content-Type: application/json on all POST requests) Auth : ApiKey scheme (see Authentication section below) Rate limit: 200 requests per minute per API key (429 if exceeded) Phone fmt : E.164 international — +255XXXXXXXXX for Tanzania Timestamps: UTC, ISO 8601 — e.g. 2026-06-07T10:00:00Z Pagination: { "count": N, "next": "url", "previous": "url", "results": [...] } ============================================================================= AUTHENTICATION ============================================================================= Every request must include BOTH your API Key (nhk_…) and Secret Key (nhs_…) in the Authorization header, joined by a colon, using the ApiKey scheme: Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY Example: Authorization: ApiKey nhk_abc123xyz789:nhs_def456uvw321 IMPORTANT: Do NOT use "Bearer". That is JWT and will return 401. Only the "ApiKey" scheme works for external API integration. Get your API keys: https://sms.newhope.co.tz/api-keys The Secret Key is shown only once at generation — save it immediately. API Key types: General — Full access to all endpoints SMS Sender — Send SMS with one specific sender ID only OTP — Only /otp/send/ and /otp/verify/ endpoints Authentication examples: cURL: curl -H "Authorization: ApiKey nhk_YOUR_KEY:nhs_YOUR_SECRET" \ https://sms.newhope.co.tz/v1/sender-ids/ Python (requests): import requests API_KEY = "nhk_YOUR_API_KEY" SECRET_KEY = "nhs_YOUR_SECRET_KEY" session = requests.Session() session.headers["Authorization"] = f"ApiKey {API_KEY}:{SECRET_KEY}" session.headers["Content-Type"] = "application/json" Node.js (axios): const api = axios.create({ baseURL: 'https://sms.newhope.co.tz/v1', headers: { 'Authorization': `ApiKey ${API_KEY}:${SECRET_KEY}` } }); PHP: $headers = ['Authorization: ApiKey ' . $apiKey . ':' . $secretKey]; Java (OkHttp): Request req = new Request.Builder() .addHeader("Authorization", "ApiKey " + apiKey + ":" + secretKey) .build(); C# (.NET HttpClient): client.DefaultRequestHeaders.Add("Authorization", $"ApiKey {apiKey}:{secretKey}"); Dart/Flutter (http package): final headers = {'Authorization': 'ApiKey $apiKey:$secretKey'}; Go: req.Header.Set("Authorization", "ApiKey " + apiKey + ":" + secretKey) Ruby: req['Authorization'] = "ApiKey #{api_key}:#{secret_key}" ============================================================================= ENDPOINTS ============================================================================= ───────────────────────────────────────────────────────────────────────────── 1. SEND SINGLE SMS ───────────────────────────────────────────────────────────────────────────── POST https://sms.newhope.co.tz/v1/sms/send/ Request body (JSON): recipient string REQUIRED E.164 phone number. Example: +255712345678 message string REQUIRED SMS body. Max 918 chars (6 segments). GSM-7: 160 chars/segment. Unicode/Swahili chars: 70 chars/segment. sender_id string optional Approved sender name (max 11 chars). Uses account default if omitted. webhook_url string optional URL to receive delivery receipt POSTs. Must respond 200 within 10 seconds. schedule_at string optional ISO 8601 future datetime for scheduled send. Response 201 Created: { "id": "9f3a1c2e-4b5d-…", "status": "sent", "recipient": "+255712345678", "sender_id_name": "NewHope", "gateway_message_id": "ATXid_xxxxxxxxxxxxx", "cost": "20.00", "segments": 1 } Notes: - Returns HTTP 402 if insufficient SMS credits. - Multi-segment messages consume multiple credits. cURL: curl -X POST https://sms.newhope.co.tz/v1/sms/send/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"recipient":"+255712345678","message":"Hello!","sender_id":"NewHope"}' Python: r = session.post(f"{BASE}/sms/send/", json={ "recipient": "+255712345678", "message": "Hello from NewHope SMS!", "sender_id": "NewHope", "webhook_url": "https://yourapp.com/dlr", }) Node.js: const { data } = await api.post('/sms/send/', { recipient: '+255712345678', message: 'Hello!', sender_id: 'NewHope' }); PHP: curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'recipient' => '+255712345678', 'message' => 'Hello from NewHope SMS!', 'sender_id' => 'NewHope', ])); Java (OkHttp): String json = "{\"recipient\":\"+255712345678\",\"message\":\"Hello!\",\"sender_id\":\"NewHope\"}"; RequestBody body = RequestBody.create(json, MediaType.get("application/json")); Request request = new Request.Builder() .url("https://sms.newhope.co.tz/v1/sms/send/") .addHeader("Authorization", "ApiKey " + apiKey + ":" + secretKey) .post(body).build(); C#: var payload = new { recipient = "+255712345678", message = "Hello!", sender_id = "NewHope" }; var res = await client.PostAsJsonAsync("https://sms.newhope.co.tz/v1/sms/send/", payload); Dart/Flutter: final res = await http.post( Uri.parse('$BASE/sms/send/'), headers: headers, body: jsonEncode({'recipient': '+255712345678', 'message': 'Hello!', 'sender_id': 'NewHope'}), ); ───────────────────────────────────────────────────────────────────────────── 2. SEND BULK SMS (BATCH) ───────────────────────────────────────────────────────────────────────────── POST https://sms.newhope.co.tz/v1/sms/send-batch/ Send the same message to every contact in a contact list in one request. Get contact list UUIDs from GET /contacts/ (see Contact Lists section below). Request body (JSON): contact_list_id string REQUIRED UUID of the contact list. message string REQUIRED SMS message body. sender_id string optional Approved sender name. schedule_at string optional ISO 8601 future send time. Response 200 OK: { "queued_count": 250, "status": "queued" } WARNING: Credits consumed = contacts × sms_segments. Check balance first: GET /profile/balance/ cURL: curl -X POST https://sms.newhope.co.tz/v1/sms/send-batch/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"contact_list_id":"LIST_UUID","message":"Hello customers!","sender_id":"NewHope"}' ───────────────────────────────────────────────────────────────────────────── 3. SMS STATUS CHECK ───────────────────────────────────────────────────────────────────────────── GET https://sms.newhope.co.tz/v1/sms/{sms_id}/status/ Check the current delivery status of a specific message using its ID (the "id" returned by /sms/send/). Response 200 OK: { "id": "9f3a1c2e-…", "status": "delivered", "sent_at": "2026-06-07T10:00:00Z", "delivered_at": "2026-06-07T10:00:05Z" } cURL: curl https://sms.newhope.co.tz/v1/sms/9f3a1c2e-4b5d-.../status/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" TIP: For real-time updates use webhook_url instead of polling. ───────────────────────────────────────────────────────────────────────────── 4. SMS REPORTS ───────────────────────────────────────────────────────────────────────────── GET https://sms.newhope.co.tz/v1/sms/reports/ Retrieve a paginated list of your sent messages and delivery statuses. Query parameters: status string Filter: pending | sent | delivered | failed page int Page number. Default: 1. page_size int Results per page. Max 100. Default: 20. Response 200 OK: { "count": 1500, "next": "…?page=2", "previous": null, "results": [{ "id": "uuid", "recipient": "+255712345678", "message_text": "Hello!", "status": "delivered", "cost": "20.00", "segments": 1, "sent_at": "2026-06-07T10:00:00Z", "delivered_at": "2026-06-07T10:00:05Z" }] } cURL: curl "https://sms.newhope.co.tz/v1/sms/reports/?status=delivered&page=1" \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" ============================================================================= CONTACT LISTS API ============================================================================= Contact lists are named groups of phone numbers used for bulk SMS campaigns. Each list has a UUID; use that UUID in /sms/send-batch/ → contact_list_id. ───────────────────────────────────────────────────────────────────────────── 5. LIST ALL CONTACT LISTS ───────────────────────────────────────────────────────────────────────────── GET https://sms.newhope.co.tz/v1/contacts/ Response 200 OK: [ { "id": "a1b2c3d4-…", "name": "Customers Q1 2026", "description": "VIP customers who purchased in Q1", "contacts_count": 1240, "created_at": "2026-01-01T09:00:00Z" } ] cURL: curl https://sms.newhope.co.tz/v1/contacts/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" Python: lists = session.get(f"{BASE}/contacts/").json() ───────────────────────────────────────────────────────────────────────────── 6. CREATE A CONTACT LIST ───────────────────────────────────────────────────────────────────────────── POST https://sms.newhope.co.tz/v1/contacts/ Request body (JSON): name string REQUIRED Name for the list. description string optional Optional description. Response 201 Created: the new contact list object. cURL: curl -X POST https://sms.newhope.co.tz/v1/contacts/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"My Customers","description":"Q1 2026 customers"}' ───────────────────────────────────────────────────────────────────────────── 7. GET / UPDATE / DELETE A CONTACT LIST ───────────────────────────────────────────────────────────────────────────── GET https://sms.newhope.co.tz/v1/contacts/{list_id}/ — Get list details PUT https://sms.newhope.co.tz/v1/contacts/{list_id}/ — Update name/description DELETE https://sms.newhope.co.tz/v1/contacts/{list_id}/ — Delete list (204 No Content) ───────────────────────────────────────────────────────────────────────────── 8. LIST CONTACTS IN A LIST ───────────────────────────────────────────────────────────────────────────── GET https://sms.newhope.co.tz/v1/contacts/{list_id}/contacts/ Query parameters: page int Page number. Default: 1. page_size int Results per page. q string Search by phone, first name, or last name. Response 200 OK: { "count": 1240, "next": "…?page=2", "previous": null, "results": [ { "id": "…", "phone_number": "+255712345678", "first_name": "Ali", "last_name": "Hassan", "created_at": "2026-01-01T09:00:00Z" } ] } ───────────────────────────────────────────────────────────────────────────── 9. ADD A SINGLE CONTACT ───────────────────────────────────────────────────────────────────────────── POST https://sms.newhope.co.tz/v1/contacts/{list_id}/contacts/ Request body (JSON): phone_number string REQUIRED E.164 phone number. Example: +255712345678 first_name string optional Contact first name. last_name string optional Contact last name. cURL: curl -X POST https://sms.newhope.co.tz/v1/contacts/LIST_UUID/contacts/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"phone_number":"+255712345678","first_name":"Ali","last_name":"Hassan"}' ───────────────────────────────────────────────────────────────────────────── 10. DELETE A CONTACT FROM A LIST ───────────────────────────────────────────────────────────────────────────── DELETE https://sms.newhope.co.tz/v1/contacts/{list_id}/contacts/{contact_id}/ Returns 204 No Content on success. ───────────────────────────────────────────────────────────────────────────── 11. BULK IMPORT CONTACTS FROM CSV ───────────────────────────────────────────────────────────────────────────── POST https://sms.newhope.co.tz/v1/contacts/{list_id}/import/ Upload a CSV file with many contacts at once. Use multipart/form-data, not JSON. Form field: file file (CSV) REQUIRED CSV with column headers: phone_number (required), first_name, last_name CSV format example: phone_number,first_name,last_name +255712345678,Ali,Hassan +255754321098,Fatuma,Mwangi +255765432109,John,Doe Response 200 OK: { "imported": 1200, "skipped": 15, "errors": ["Row 5: invalid phone"], "total_in_list": 2440 } cURL: curl -X POST https://sms.newhope.co.tz/v1/contacts/LIST_UUID/import/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" \ -F "file=@contacts.csv" Python: with open("contacts.csv", "rb") as f: r = requests.post( f"{BASE}/contacts/LIST_UUID/import/", files={"file": ("contacts.csv", f, "text/csv")}, headers={"Authorization": f"ApiKey {API_KEY}:{SECRET_KEY}"}, ) Node.js (form-data): const FormData = require('form-data'); const form = new FormData(); form.append('file', fs.createReadStream('contacts.csv')); await axios.post('/contacts/LIST_UUID/import/', form, { headers: form.getHeaders() }); ============================================================================= SENDER IDs ============================================================================= ───────────────────────────────────────────────────────────────────────────── 12. LIST SENDER IDs ───────────────────────────────────────────────────────────────────────────── GET https://sms.newhope.co.tz/v1/sender-ids/ Returns all sender IDs on your account. Only sender IDs with status "approved" can be used for sending. Response 200 OK: [ { "id": "a1b2c3d4-…", "sender_name": "NewHope", "status": "approved", "is_default": true, "approved_at": "2026-01-10T08:00:00Z", "rejection_reason": null } ] Sender ID status values: awaiting_payment — Application fee not yet paid pending — Fee paid, compliance review in 1–2 business days under_review — Under review by NewHope compliance team approved — Active and ready to use rejected — Denied — see rejection_reason field To request a new Sender ID: - Log in → Sender IDs → Request New - Upload Brela / TIN / Business Licence (PDF) - Pay TZS 5,000 non-refundable fee via mobile money - Wait 1–2 business days for compliance review - Rules: max 11 characters, letters and numbers only, no spaces cURL: curl https://sms.newhope.co.tz/v1/sender-ids/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" ============================================================================= OTP ============================================================================= ───────────────────────────────────────────────────────────────────────────── 13. SEND OTP ───────────────────────────────────────────────────────────────────────────── POST https://sms.newhope.co.tz/v1/otp/send/ Send a 6-digit one-time password via SMS. OTP expires after 5 minutes (300s). Requires OTP or General API key. Request body (JSON): phone string REQUIRED Recipient phone in E.164 format. sender_id string optional Sender name. Uses account default if omitted. Response 201 Created: { "request_id": "uuid-…", "expires_in": 300 } cURL: curl -X POST https://sms.newhope.co.tz/v1/otp/send/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"phone":"+255712345678","sender_id":"NewHope"}' ───────────────────────────────────────────────────────────────────────────── 14. VERIFY OTP ───────────────────────────────────────────────────────────────────────────── POST https://sms.newhope.co.tz/v1/otp/verify/ Verify the code submitted by the user against the request_id from /otp/send/. Request body (JSON): request_id uuid REQUIRED Returned by /otp/send/ otp string REQUIRED 6-digit code entered by the user. Response 200 OK: { "valid": true } — code is correct and not expired { "valid": false } — code is wrong or expired cURL: curl -X POST https://sms.newhope.co.tz/v1/otp/verify/ \ -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"request_id":"uuid-…","otp":"847291"}' Python (full flow): send = session.post(f"{BASE}/otp/send/", json={"phone": "+255712345678"}) req_id = send.json()["request_id"] verify = session.post(f"{BASE}/otp/verify/", json={"request_id": req_id, "otp": user_input}) is_valid = verify.json()["valid"] ============================================================================= WEBHOOKS / DELIVERY REPORTS (DLR) ============================================================================= Set webhook_url in your send request. NewHope SMS will POST to that URL every time the delivery status of a message changes. Your endpoint MUST respond with HTTP 200 within 10 seconds. Failed POSTs are retried up to 3 times with exponential back-off. Webhook payload (POST to your URL): { "message_id": "9f3a1c2e-…", "recipient": "+255712345678", "status": "delivered", "delivered_at": "2026-06-07T10:00:05Z", "gateway": "beem" } Status values: sent | delivered | failed | pending Webhook handlers: Python / Flask: @app.route('/dlr', methods=['POST']) def dlr(): d = request.json print(f"[{d['status']}] {d['recipient']} — {d['message_id']}") return 'OK', 200 # MUST return 200 Node.js / Express: app.post('/dlr', (req, res) => { const { message_id, recipient, status } = req.body; if (status === 'delivered') { /* update DB */ } res.sendStatus(200); // MUST return 200 }); PHP: $d = json_decode(file_get_contents('php://input'), true); http_response_code(200); echo 'OK'; Java / Spring Boot: @PostMapping("/dlr") public ResponseEntity dlr(@RequestBody Map payload) { return ResponseEntity.ok("OK"); // MUST return 200 } C# / ASP.NET Core: [HttpPost("/dlr")] public IActionResult Dlr([FromBody] JsonElement payload) { return Ok("OK"); } ============================================================================= ERROR HANDLING ============================================================================= All errors return JSON: { "error": "Human-readable description" } or { "detail": "…" } | HTTP | Meaning | |------|-------------------------------------------------------------------| | 400 | Bad Request — missing or invalid parameters | | 401 | Unauthorized — use ApiKey scheme, not Bearer. Check both keys. | | 402 | Payment Required — top up at sms.newhope.co.tz/purchases | | 403 | Forbidden — account restricted or key type not permitted | | 404 | Not Found — resource UUID not found | | 429 | Too Many Requests — 200 req/min limit, retry with back-off | | 500 | Internal Server Error — contact support@newhope.co.tz | Retry pattern (Python): import time for attempt in range(3): r = session.post(f"{BASE}/sms/send/", json=payload) if r.status_code == 201: break if r.status_code == 429 or r.status_code >= 500: time.sleep(2 ** attempt) # 1s, 2s, 4s continue r.raise_for_status() # 4xx — don't retry ============================================================================= PRICING (TZS per SMS credit) ============================================================================= Credits are pre-purchased and never expire. | SMS Range | Price per SMS (TZS) | Saving | |---------------------|---------------------|---------| | 1 – 10,000 | 20.00 | — | | 10,001 – 25,000 | 18.00 | 10% off | | 25,001 – 50,000 | 17.00 | 15% off | | 50,001 – 100,000 | 16.00 | 20% off | | 100,001 – 250,000 | 14.00 | 30% off | | 250,001 – 500,000 | 13.00 | 35% off | | 500,001 – 1,000,000 | 12.00 | 40% off | | 1,000,001+ | 10.70 | 46.5% off| Payment methods: M-Pesa (Vodacom), Airtel Money, Tigo Pesa, TTCL Pesa, Halotel, Bank Transfer. Credits added instantly after mobile money confirmation. Buy credits: https://sms.newhope.co.tz/purchases ============================================================================= TOOLS & LIBRARIES ============================================================================= OpenAPI 3.0 JSON spec (import into Postman / Insomnia / Swagger UI): https://sms.newhope.co.tz/openapi.json Postman: File → Import → URL → paste the URL above Insomnia: Import → OpenAPI → paste the URL above Recommended HTTP libraries: Python — requests (pip install requests) Node.js — axios or fetch (npm install axios) PHP — cURL (built-in) or GuzzleHTTP Java — OkHttp or Java 11 HttpClient Android — OkHttp or Retrofit C# / .NET — System.Net.Http.HttpClient (built-in) Dart/Flutter — http package (flutter pub add http) Go — net/http (built-in, no dependencies) Ruby — Net::HTTP (built-in) or Faraday Kotlin — OkHttp or Ktor Swift/iOS — URLSession (built-in) or Alamofire ============================================================================= SUPPORT ============================================================================= Email : support@newhope.co.tz Website : https://sms.newhope.co.tz Login : https://sms.newhope.co.tz/accounts/login/ Sign up : https://sms.newhope.co.tz/accounts/register/ Docs : https://sms.newhope.co.tz/docs (full HTML with code examples) Plain : https://sms.newhope.co.tz/llms.txt (AI-readable, this file) OpenAPI : https://sms.newhope.co.tz/openapi.json (Postman / Insomnia import)