📖 API Reference

NewHope SMS API

A complete REST API to send SMS, OTP, and delivery reports in Tanzania. Works with every programming language — if it can make an HTTP request, it works with NewHope SMS.

https://sms.newhope.co.tz/v1 JSON 200 req/min HTTPS only Postman / OpenAPI

⚡ Quick Start — Send Your First SMS in 5 Minutes

Follow these 3 steps. No SDK installation required — just HTTP.

1

Create an account and generate API keys

Sign up at sms.newhope.co.tz/accounts/register/ → go to API Keys → click Generate New Key → choose a sender ID → save both keys shown to you. The Secret Key (nhs_…) is shown only once.

2

Send your first SMS

Replace YOUR_API_KEY and YOUR_SECRET_KEY in the cURL command below, then run it in any terminal. You should receive an SMS on the phone number you specified.

cURL — works on Linux, macOS, Windows, Git Bash, WSL
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"}'
3

Check the response

A successful response looks like this. The id is your message ID — use it to check delivery status.

Response — 201 Created
{
  "id":                 "9f3a1c2e-4b5d-…",
  "status":             "sent",
  "recipient":          "+255712345678",
  "sender_id_name":     "NewHope",
  "gateway_message_id": "ATXid_xxxxxxxxxxxxx",
  "cost":               "20.00",
  "segments":           1
}
💡

Any language. Any framework. cURL above is just a raw HTTP POST. In your code, use whatever HTTP client your language/framework provides — requests (Python), axios/fetch (JS), HttpClient (.NET), OkHttp (Java/Android), http package (Dart/Flutter), curl_exec (PHP). They all send the same HTTP request.

Introduction

The NewHope SMS API is a RESTful HTTP API targeting businesses, schools, hospitals, NGOs, churches, and developers in Tanzania.

PropertyValue
Base URLhttps://sms.newhope.co.tz/v1
ProtocolHTTPS only — plain HTTP is rejected
FormatJSON — set Content-Type: application/json on all POST/PUT requests
AuthenticationAuthorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY
Rate limit200 requests per minute per API key (429 if exceeded)
Phone formatE.164 international — +255XXXXXXXXX for Tanzania
TimestampsUTC, ISO 8601 format — e.g. 2026-06-07T10:00:00Z
PaginationPaginated lists return count, next, previous, results
Supportsupport@newhope.co.tz

Authentication

Every request must include both your API Key and your Secret Key in the Authorization header, joined by a colon, using the ApiKey scheme.

Authorization header format
Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY
API Key  nhk_…
Visible any time in the dashboard → API Keys. Identifies your account.
Secret Key  nhs_…
Shown once at generation only. Copy it immediately to a secure place — it cannot be recovered.

Do not use Authorization: Bearer nhk_…. That format is JWT and will return 401 Token is invalid or expired. External API clients must use ApiKey key:secret.

API Key types

TypeAccess
GeneralFull access — all endpoints
SMS SenderSend SMS using one specific sender ID only
OTPOnly /otp/send/ and /otp/verify/

Generate keys: sms.newhope.co.tz/api-keys

Code examples — Authentication setup

cURL (universal — works on any OS, any language)
curl https://sms.newhope.co.tz/v1/sender-ids/ \
  -H "Authorization: ApiKey nhk_YOUR_API_KEY:nhs_YOUR_SECRET_KEY"
🐍 Python (requests)
Python
import requests

API_KEY    = "nhk_YOUR_API_KEY"    # dashboard → API Keys
SECRET_KEY = "nhs_YOUR_SECRET_KEY" # save once, never shown again
BASE       = "https://sms.newhope.co.tz/v1"

session = requests.Session()
session.headers.update({
    "Authorization": f"ApiKey {API_KEY}:{SECRET_KEY}",
    "Content-Type": "application/json",
})

r = session.get(f"{BASE}/sender-ids/")
print(r.json())
🟨 JavaScript / Node.js (axios)
Node.js — axios
const axios = require('axios');

const API_KEY    = 'nhk_YOUR_API_KEY';    // dashboard → API Keys
const SECRET_KEY = 'nhs_YOUR_SECRET_KEY'; // save once, never shown again

const api = axios.create({
  baseURL: 'https://sms.newhope.co.tz/v1',
  headers: {
    'Authorization': `ApiKey ${API_KEY}:${SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
});

const { data } = await api.get('/sender-ids/');
console.log(data);
🟦 JavaScript / Browser (fetch)
Browser / Deno / Bun — native fetch
const API_KEY    = 'nhk_YOUR_API_KEY';
const SECRET_KEY = 'nhs_YOUR_SECRET_KEY';

const headers = {
  'Authorization': `ApiKey ${API_KEY}:${SECRET_KEY}`,
  'Content-Type': 'application/json',
};

const res  = await fetch('https://sms.newhope.co.tz/v1/sender-ids/', { headers });
const data = await res.json();
console.log(data);
🐘 PHP (cURL / GuzzleHTTP)
PHP — cURL
<?php
$apiKey    = 'nhk_YOUR_API_KEY';    // dashboard → API Keys
$secretKey = 'nhs_YOUR_SECRET_KEY'; // save once, never shown again

$ch = curl_init('https://sms.newhope.co.tz/v1/sender-ids/');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: ApiKey ' . $apiKey . ':' . $secretKey,
    ],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($data);
PHP — GuzzleHTTP (if installed)
<?php
use GuzzleHttp\Client;

$client = new Client([
    'base_uri' => 'https://sms.newhope.co.tz/v1/',
    'headers'  => [
        'Authorization' => 'ApiKey ' . $apiKey . ':' . $secretKey,
        'Content-Type'  => 'application/json',
    ],
]);

$res  = $client->get('sender-ids/');
$data = json_decode($res->getBody(), true);
☕ Java (OkHttp / HttpClient)
Java — OkHttp (Android / Spring / Desktop)
import okhttp3.*;

String apiKey    = "nhk_YOUR_API_KEY";
String secretKey = "nhs_YOUR_SECRET_KEY";

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
    .url("https://sms.newhope.co.tz/v1/sender-ids/")
    .addHeader("Authorization", "ApiKey " + apiKey + ":" + secretKey)
    .build();

try (Response response = client.newCall(request).execute()) {
    System.out.println(response.body().string());
}
Java — HttpClient (Java 11+, no extra libraries)
import java.net.http.*;
import java.net.URI;

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://sms.newhope.co.tz/v1/sender-ids/"))
    .header("Authorization", "ApiKey " + apiKey + ":" + secretKey)
    .GET()
    .build();

HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
🔷 C# / .NET (HttpClient)
C# — HttpClient (.NET 6+, .NET MAUI, Xamarin, ASP.NET)
using System.Net.Http;
using System.Net.Http.Json;

var apiKey    = "nhk_YOUR_API_KEY";
var secretKey = "nhs_YOUR_SECRET_KEY";

var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", $"ApiKey {apiKey}:{secretKey}");

var senderIds = await client.GetFromJsonAsync<List<object>>(
    "https://sms.newhope.co.tz/v1/sender-ids/");
Console.WriteLine(senderIds);
🎯 Dart / Flutter (http package)
Dart / Flutter — http package
import 'package:http/http.dart' as http;
import 'dart:convert';

// Add to pubspec.yaml: http: ^1.2.0

const apiKey    = 'nhk_YOUR_API_KEY';
const secretKey = 'nhs_YOUR_SECRET_KEY';

final headers = {
  'Authorization': 'ApiKey $apiKey:$secretKey',
  'Content-Type': 'application/json',
};

final res = await http.get(
  Uri.parse('https://sms.newhope.co.tz/v1/sender-ids/'),
  headers: headers,
);
final data = jsonDecode(res.body);
print(data);
🦀 Go (net/http)
Go — standard library, no dependencies
package main

import (
    "fmt"; "io"; "net/http"
)

func main() {
    apiKey    := "nhk_YOUR_API_KEY"
    secretKey := "nhs_YOUR_SECRET_KEY"

    req, _ := http.NewRequest("GET",
        "https://sms.newhope.co.tz/v1/sender-ids/", nil)
    req.Header.Set("Authorization", "ApiKey "+apiKey+":"+secretKey)

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}
💎 Ruby (Net::HTTP / Faraday)
Ruby — standard library
require 'net/http'
require 'json'

api_key    = 'nhk_YOUR_API_KEY'
secret_key = 'nhs_YOUR_SECRET_KEY'

uri = URI('https://sms.newhope.co.tz/v1/sender-ids/')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "ApiKey #{api_key}:#{secret_key}"

res  = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
data = JSON.parse(res.body)
puts data

Send SMS

Send a single SMS to one recipient. Consumes 1 credit per segment.

POSThttps://sms.newhope.co.tz/v1/sms/send/

Request body (JSON)

FieldTypeStatusDescription
recipientstringRequiredE.164 phone — +255712345678
messagestringRequiredSMS body. Max 918 chars (6 segments). GSM-7: 160 chars/segment. Unicode/Swahili: 70 chars/segment.
sender_idstringOptionalApproved sender name (max 11 chars). Uses account default if omitted.
webhook_urlstring URLOptionalURL to receive delivery receipt POSTs. Must respond 200 within 10 seconds.
schedule_atdatetimeOptionalISO 8601 future datetime — e.g. 2026-06-10T09:00:00Z

Response — 201 Created

JSON
{
  "id":                 "9f3a1c2e-4b5d-…",
  "status":             "sent",
  "recipient":          "+255712345678",
  "sender_id_name":     "NewHope",
  "gateway_message_id": "ATXid_xxxxxxxxxxxxx",
  "cost":               "20.00",
  "segments":           1
}

Code examples

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 from NewHope SMS!","sender_id":"NewHope"}'
🐍 Python
Python
r = session.post("/sms/send/", json={
    "recipient":   "+255712345678",
    "message":     "Hello from NewHope SMS!",
    "sender_id":   "NewHope",
    "webhook_url": "https://yourapp.com/dlr",
})
print(r.json())  # {'id': '...', 'status': 'sent', 'cost': '20.00'}
🟨 Node.js
Node.js
const { data } = await api.post('/sms/send/', {
  recipient:   '+255712345678',
  message:     'Hello from NewHope SMS!',
  sender_id:   'NewHope',
  webhook_url: 'https://yourapp.com/dlr',
});
🐘 PHP
PHP
$ch = curl_init('https://sms.newhope.co.tz/v1/sms/send/');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true,
    CURLOPT_HTTPHEADER     => ['Authorization: ApiKey '.$apiKey.':'.$secretKey, 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode(['recipient'=>'+255712345678','message'=>'Hello!','sender_id'=>'NewHope']),
]);
print_r(json_decode(curl_exec($ch), true)); curl_close($ch);
☕ Java
Java — OkHttp
String body = """{"recipient":"+255712345678","message":"Hello!","sender_id":"NewHope"}""";
RequestBody rb = RequestBody.create(body, MediaType.get("application/json"));
Request req = new Request.Builder()
    .url("https://sms.newhope.co.tz/v1/sms/send/")
    .addHeader("Authorization", "ApiKey " + apiKey + ":" + secretKey)
    .post(rb).build();
try (Response r = client.newCall(req).execute()) { System.out.println(r.body().string()); }
🔷 C#
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);
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(data);
🎯 Dart / Flutter
Dart
final res = await http.post(
  Uri.parse('https://sms.newhope.co.tz/v1/sms/send/'),
  headers: headers,
  body: jsonEncode({'recipient': '+255712345678', 'message': 'Hello!', 'sender_id': 'NewHope'}),
);
print(jsonDecode(res.body));

Bulk SMS

Send the same message to an entire contact list in one request.

POSThttps://sms.newhope.co.tz/v1/sms/send-batch/
FieldTypeStatusDescription
contact_list_iduuidRequiredUUID of the contact list. Get it from GET /contacts/
messagestringRequiredSMS message body.
sender_idstringOptionalApproved sender name. Uses default if omitted.
schedule_atdatetimeOptionalISO 8601 future send time.

Response — 200 OK

JSON
{ "queued_count": 250, "status": "queued" }

Credits consumed = contacts × message segments. Ensure sufficient balance before sending. Returns 402 if insufficient.

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":"YOUR_LIST_UUID","message":"Hello customers!","sender_id":"NewHope"}'

SMS Status

Check the current delivery status of a specific message by its ID.

GEThttps://sms.newhope.co.tz/v1/sms/{sms_id}/status/

Response — 200 OK

JSON
{
  "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"
💡

If you prefer real-time delivery updates instead of polling, use the webhook_url field in your send request. NewHope SMS will POST to your URL the moment status changes.

SMS Reports

Retrieve a paginated list of your sent messages and delivery statuses.

GEThttps://sms.newhope.co.tz/v1/sms/reports/
Query ParamTypeStatusDescription
statusstringOptionalpending · sent · delivered · failed
pageintegerOptionalPage number. Default: 1.
page_sizeintegerOptionalResults per page. Max 100. Default: 20.
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

Manage contact lists used for bulk SMS campaigns. A contact list is a named group of phone numbers.

List all contact lists

GEThttps://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"

Create a contact list

POSThttps://sms.newhope.co.tz/v1/contacts/
FieldTypeStatusDescription
namestringRequiredName of the contact list.
descriptionstringOptionalOptional description.
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"}'

Delete a contact list

DELETEhttps://sms.newhope.co.tz/v1/contacts/{list_id}/

Returns 204 No Content on success.

List contacts in a list

GEThttps://sms.newhope.co.tz/v1/contacts/{list_id}/contacts/
Query ParamTypeDescription
pageintegerPage number. Default: 1.
page_sizeintegerResults per page.
qstringSearch 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": "…" }
  ]
}

Add a single contact

POSThttps://sms.newhope.co.tz/v1/contacts/{list_id}/contacts/
FieldTypeStatusDescription
phone_numberstringRequiredE.164 phone number — +255712345678
first_namestringOptionalContact first name.
last_namestringOptionalContact 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"}'

Remove a contact

DELETEhttps://sms.newhope.co.tz/v1/contacts/{list_id}/contacts/{contact_id}/

Returns 204 No Content on success.

Bulk import contacts from CSV

POSThttps://sms.newhope.co.tz/v1/contacts/{list_id}/import/

Upload a CSV file to import many contacts at once. Use multipart/form-data, not JSON.

FieldTypeDescription
filefile (CSV)CSV with column headers: phone_number, first_name (optional), last_name (optional)
CSV format example
phone_number,first_name,last_name
+255712345678,Ali,Hassan
+255754321098,Fatuma,Mwangi
+255765432109,John,Doe
cURL — file upload
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"
Response — 200 OK
{
  "imported":     1200,
  "skipped":      15,
  "errors":       ["Row 5: invalid phone number"],
  "total_in_list": 2440
}
🐍 Python — CSV import
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}"},
    )
print(r.json())  # {'imported': 1200, 'skipped': 15, ...}
🟨 Node.js — CSV import
Node.js — form-data
const FormData = require('form-data');
const fs = require('fs');

const form = new FormData();
form.append('file', fs.createReadStream('contacts.csv'));

const { data } = await axios.post('/contacts/LIST_UUID/import/', form, {
  headers: { ...form.getHeaders() },
});
console.log(data);
🎯 Dart / Flutter — CSV import
Dart
var request = http.MultipartRequest(
  'POST',
  Uri.parse('https://sms.newhope.co.tz/v1/contacts/LIST_UUID/import/'),
);
request.headers['Authorization'] = 'ApiKey $apiKey:$secretKey';
request.files.add(await http.MultipartFile.fromPath('file', 'contacts.csv'));
var response = await request.send();
print(await response.stream.bytesToString());

Sender IDs

List sender IDs on your account. Only approved sender IDs can be used. The default "NewHope" sender ID is pre-approved on every account.

GEThttps://sms.newhope.co.tz/v1/sender-ids/
Response — 200 OK
[
  {
    "id":               "a1b2c3d4-…",
    "sender_name":      "NewHope",
    "status":           "approved",
    "is_default":       true,
    "approved_at":      "2026-01-10T08:00:00Z",
    "rejection_reason": null
  }
]
StatusMeaning
awaiting_paymentApplication fee (TZS 5,000) not yet paid
pendingFee paid — compliance review in 1–2 business days
under_reviewUnder review by the NewHope compliance team
approvedActive — can be used for sending
rejectedDenied — reason in rejection_reason field

To apply for a custom sender ID: log in → Sender IDs → Request New. Upload Brela / TIN / Business Licence (PDF), pay TZS 5,000 via mobile money. Sender ID names: max 11 characters, letters and numbers only, no spaces.

OTP — Send

Send a 6-digit one-time password via SMS. Expires after 5 minutes (300 seconds). Requires OTP or General API key.

POSThttps://sms.newhope.co.tz/v1/otp/send/
FieldTypeStatusDescription
phonestringRequiredRecipient phone in E.164 format
sender_idstringOptionalSender 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"}'

OTP — Verify

Verify the 6-digit code submitted by the user against the request_id from /otp/send/.

POSThttps://sms.newhope.co.tz/v1/otp/verify/
FieldTypeStatusDescription
request_iduuidRequiredThe request_id returned by /otp/send/
otpstringRequired6-digit code entered by the user
Response — 200 OK
{ "valid": true }   // or { "valid": false } if 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 OTP flow
Python
# Step 1 — send OTP
send = session.post(f"{BASE}/otp/send/", json={"phone": "+255712345678", "sender_id": "NewHope"})
request_id = send.json()["request_id"]
print(f"OTP sent — expires in {send.json()['expires_in']}s")

# Step 2 — user reads code; then verify
verify = session.post(f"{BASE}/otp/verify/", json={"request_id": request_id, "otp": user_input})
print("Valid:", verify.json()["valid"])
🎯 Dart / Flutter — Full OTP flow
Dart
// Step 1 — send OTP
final sendRes = await http.post(Uri.parse('$BASE/otp/send/'), headers: headers,
  body: jsonEncode({'phone': '+255712345678', 'sender_id': 'NewHope'}));
final requestId = jsonDecode(sendRes.body)['request_id'];

// Step 2 — verify OTP
final verifyRes = await http.post(Uri.parse('$BASE/otp/verify/'), headers: headers,
  body: jsonEncode({'request_id': requestId, 'otp': userInput}));
final valid = jsonDecode(verifyRes.body)['valid'];
print('OTP valid: $valid');

Webhooks / Delivery Reports (DLR)

Set webhook_url in your send request. NewHope SMS will POST to that URL every time delivery status changes.

Your endpoint must respond with HTTP 200 within 10 seconds. Failed deliveries are retried up to 3 times with exponential back-off.

Payload — POST to your URL

JSON
{
  "message_id":   "9f3a1c2e-…",
  "recipient":    "+255712345678",
  "status":       "delivered",
  "delivered_at": "2026-06-07T10:00:05Z",
  "gateway":      "beem"
}
sent delivered failed pending
🐍 Python — Flask webhook handler
Python / Flask
from flask import Flask, request
app = Flask(__name__)

@app.route('/dlr', methods=['POST'])
def dlr():
    d = request.json
    print(f"[{d['status'].upper()}] {d['recipient']} — {d['message_id']}")
    if d['status'] == 'delivered':
        pass  # update DB, send follow-up, etc.
    return 'OK', 200  # MUST return 200
🟨 Node.js — Express webhook handler
Node.js / Express
app.post('/dlr', (req, res) => {
  const { message_id, recipient, status } = req.body;
  console.log(`[${status.toUpperCase()}] ${recipient} — ${message_id}`);
  if (status === 'delivered') { /* update DB */ }
  res.sendStatus(200);  // MUST return 200
});
🐘 PHP webhook handler
PHP
$d = json_decode(file_get_contents('php://input'), true);
error_log("DLR [{$d['status']}] {$d['recipient']} — {$d['message_id']}");
if ($d['status'] === 'delivered') { /* update DB */ }
http_response_code(200); echo 'OK';
☕ Java — Spring Boot webhook
Java / Spring Boot
@PostMapping("/dlr")
public ResponseEntity<String> dlr(@RequestBody Map<String, Object> payload) {
    String status = (String) payload.get("status");
    log.info("DLR [{}] {} — {}", status, payload.get("recipient"), payload.get("message_id"));
    if ("delivered".equals(status)) { /* update DB */ }
    return ResponseEntity.ok("OK");  // MUST return 200
}
🔷 C# — ASP.NET Core webhook
C# / ASP.NET Core
[HttpPost("/dlr")]
public IActionResult Dlr([FromBody] JsonElement payload) {
    var status = payload.GetProperty("status").GetString();
    var recipient = payload.GetProperty("recipient").GetString();
    _logger.LogInformation("DLR [{status}] {recipient}", status, recipient);
    if (status == "delivered") { /* update DB */ }
    return Ok("OK");  // MUST return 200
}

Error Handling Best Practices

Build resilient integrations using these patterns.

Error response format

JSON — error body
{ "error": "Insufficient SMS credits." }
{ "detail": "Authentication credentials were not provided." }

Retry with exponential back-off (for 429 / 5xx)

🐍 Python — retry pattern
Python
import time, requests

def send_sms_with_retry(session, payload, max_retries=3):
    for attempt in range(max_retries):
        r = session.post(f"{BASE}/sms/send/", json=payload)
        if r.status_code == 201:
            return r.json()
        if r.status_code == 429 or r.status_code >= 500:
            wait = 2 ** attempt  # 1s, 2s, 4s
            time.sleep(wait)
            continue
        r.raise_for_status()  # 4xx errors — don't retry
    raise Exception("Max retries exceeded")
🟨 Node.js — retry pattern
Node.js
async function sendWithRetry(payload, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const { data } = await api.post('/sms/send/', payload);
      return data;
    } catch (err) {
      const status = err.response?.status;
      if (status === 429 || status >= 500) {
        await new Promise(r => setTimeout(r, 1000 * 2 ** i));
        continue;
      }
      throw err;  // 4xx — don't retry
    }
  }
  throw new Error('Max retries exceeded');
}

Rate limit headers

When approaching the rate limit (200 req/min), the API returns 429 Too Many Requests. Back off and retry after a short delay. Do not retry immediately.

Balance checks (avoid 402)

cURL — check balance before large sends
curl https://sms.newhope.co.tz/v1/profile/balance/ \
  -H "Authorization: ApiKey YOUR_API_KEY:YOUR_SECRET_KEY"
# Returns: {"sms_balance": 5000, "currency": "TZS"}

Error Code Reference

400
Bad Request
Missing or invalid parameter. Check field names, required fields, and value formats.
401
Unauthorized
Missing, wrong-format, or invalid/revoked credentials. Use Authorization: ApiKey nhk_…:nhs_…. Do not use Bearer.
402
Payment Required
Insufficient SMS credits. Top up at sms.newhope.co.tz/purchases
403
Forbidden
Account restricted, or API key type does not permit this action.
404
Not Found
Resource (contact list UUID, sender ID, message ID) does not exist.
429
Too Many Requests
Rate limit exceeded (200 req/min). Back off and retry with exponential delay.
500
Internal Server Error
Unexpected server error. Contact support@newhope.co.tz if it persists.

Pricing (TZS per SMS credit)

Credits are pre-purchased and never expire. The more you buy, the lower the per-SMS cost.

SMS RangePrice per SMS (TZS)
1 – 10,000 TZS 20.00
10,001 – 25,000 TZS 18.00
25,001 – 50,000 TZS 17.00
50,001 – 100,000 TZS 16.00
100,001 – 250,000 TZS 14.00
250,001 – 500,000 TZS 13.00
500,001 – 1,000,000 TZS 12.00
1,000,001 – 2,000,000 TZS 10.70

Payment methods: M-Pesa (Vodacom), Airtel Money, Tigo Pesa, TTCL Pesa, Halotel, Bank Transfer — added instantly after mobile money confirmation.

Buy credits: sms.newhope.co.tz/purchases

Tools & Libraries

Import the OpenAPI spec into any API client to get auto-generated documentation, request builders, and code snippets in your language.

Recommended HTTP libraries by language

Language / PlatformRecommended libraryInstall
Pythonrequestspip install requests
Node.jsaxios or native fetchnpm install axios
Browser JSNative fetch (built-in)
PHPcURL (built-in) or GuzzleHTTPcomposer require guzzlehttp/guzzle
JavaOkHttp or Java 11 HttpClientimplementation 'com.squareup.okhttp3:okhttp:4.12.0'
AndroidOkHttp or Retrofitimplementation 'com.squareup.retrofit2:retrofit:2.11.0'
C# / .NETSystem.Net.Http.HttpClient (built-in)
Dart / Flutterhttp packageflutter pub add http
Gonet/http (built-in)
RubyNet::HTTP (built-in) or faradaygem install faraday
KotlinOkHttp or Ktorimplementation 'io.ktor:ktor-client-core:2.3.12'
Swift / iOSURLSession (built-in) or Alamofirepod 'Alamofire'

Need help integrating?

Our team assists with API integration questions, Sender ID approvals, and billing issues.