Developer Reference

eHub SMS REST API

Send SMS, manage sender IDs, and check your wallet programmatically. All endpoints require HMAC-signed requests.

BASE URL https://sms.ehub.co.tz/api/v1

Overview

Everything you need to integrate eHub SMS into your application.

To get your API key and secret, log in to your dashboard → Developer → API Keys → Generate new key. Both values are shown only once at creation — store them securely.

The eHub SMS API is a REST API that accepts JSON request bodies and returns JSON responses. Every request must be authenticated with a Bearer token and signed with an HMAC-SHA256 signature to prevent replay attacks.

Authentication

Every request requires your API key in the Authorization header.

Required header
Authorization: Bearer YOUR_API_KEY

Your API key identifies your account. Keep it secret — never expose it in client-side code or public repositories. All requests must also include a valid HMAC signature (see below).

Request Signing

Every request must be signed with HMAC-SHA256 using your API secret.

Requests without a valid signature or with a timestamp older than 5 minutes will be rejected with 401 Unauthorized.

Signing algorithm

Signature payload (newline-separated)
# Concatenate these four values with newlines
{unix_timestamp}
{HTTP_METHOD}
{/full/path}
{raw_body}

# Signature
signature = HMAC-SHA256(secret, payload)  # hex-encoded

Required headers

HeaderValueNotes
X-TimestampUnix timestamp (integer)Must be within ±5 min of server time
X-SignatureHMAC-SHA256 hex stringSigned with your API secret
For GET requests, use an empty string "" as the body. Timestamps use Unix time (seconds since epoch) — timezone doesn't matter.

Code examples

// PHP signing example
$apiKey  = 'sk_your_api_key';
$secret  = 'your_api_secret';
$method  = 'POST';
$path    = '/api/v1/sms/send';
$body    = json_encode(['to' => '255755000000', 'message' => 'Hello!', 'sender_id' => 'uuid-here']);

$timestamp = time();
$payload   = $timestamp . "\n" . $method . "\n" . $path . "\n" . $body;
$signature = hash_hmac('sha256', $payload, $secret);

$response = Http::withHeaders([
    'Authorization' => 'Bearer ' . $apiKey,
    'X-Timestamp'   => $timestamp,
    'X-Signature'   => $signature,
    'Content-Type'  => 'application/json',
])->post('https://sms.ehub.co.tz/api/v1/sms/send', json_decode($body, true));
# Python signing example
import hmac, hashlib, time, json, requests

api_key = 'sk_your_api_key'
secret  = 'your_api_secret'
method  = 'POST'
path    = '/api/v1/sms/send'
body    = json.dumps({'to': '255755000000', 'message': 'Hello!', 'sender_id': 'uuid-here'})

timestamp = str(int(time.time()))
payload   = f"{timestamp}\n{method}\n{path}\n{body}"
signature = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()

response = requests.post(
    'https://sms.ehub.co.tz/api/v1/sms/send',
    headers={
        'Authorization': f'Bearer {api_key}',
        'X-Timestamp':   timestamp,
        'X-Signature':   signature,
        'Content-Type':  'application/json',
    },
    data=body
)
// Go signing example
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "net/http"
    "strings"
    "strconv"
    "time"
)

func sign(secret, method, path, body string) (string, string) {
    ts := strconv.FormatInt(time.Now().Unix(), 10)
    payload := ts + "\n" + method + "\n" + path + "\n" + body
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(payload))
    return ts, hex.EncodeToString(mac.Sum(nil))
}

func main() {
    apiKey := "sk_your_api_key"
    secret := "your_api_secret"
    body   := `{"to":"255755000000","message":"Hello!","sender_id":"uuid-here"}`

    ts, sig := sign(secret, "POST", "/api/v1/sms/send", body)

    req, _ := http.NewRequest("POST",
        "https://sms.ehub.co.tz/api/v1/sms/send",
        strings.NewReader(body))

    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("X-Timestamp", ts)
    req.Header.Set("X-Signature", sig)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    fmt.Println(resp.Status, err)
}
// Java signing example
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.http.*;
import java.time.Instant;

public class EhubSmsClient {

    static String sign(String secret, String method, String path, String body) throws Exception {
        String ts      = String.valueOf(Instant.now().getEpochSecond());
        String payload = ts + "\n" + method + "\n" + path + "\n" + body;

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
        byte[] raw = mac.doFinal(payload.getBytes());

        StringBuilder hex = new StringBuilder();
        for (byte b : raw) hex.append(String.format("%02x", b));
        return ts + ":" + hex;   // returns "timestamp:signature"
    }

    public static void main(String[] args) throws Exception {
        String apiKey = "sk_your_api_key";
        String secret = "your_api_secret";
        String body   = "{\"to\":\"255755000000\",\"message\":\"Hello!\",\"sender_id\":\"uuid-here\"}";

        String[] parts = sign(secret, "POST", "/api/v1/sms/send", body).split(":", 2);
        String ts  = parts[0];
        String sig = parts[1];

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://sms.ehub.co.tz/api/v1/sms/send"))
            .header("Authorization", "Bearer " + apiKey)
            .header("X-Timestamp",   ts)
            .header("X-Signature",   sig)
            .header("Content-Type",  "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> resp =
            HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(resp.body());
    }
}
// Node.js signing example
const crypto = require('crypto');

const API_KEY  = 'sk_your_api_key';
const SECRET   = 'your_api_secret';
const BASE_URL = 'https://sms.ehub.co.tz';

async function sendSms(to, message, senderId) {
  const path = '/api/v1/sms/send';
  const body = JSON.stringify({ to, message, sender_id: senderId });
  const timestamp = Math.floor(Date.now() / 1000);

  const payload = [timestamp, 'POST', path, body].join('\n');
  const signature = crypto
    .createHmac('sha256', SECRET)
    .update(payload)
    .digest('hex');

  const res = await fetch(BASE_URL + path, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'X-Timestamp':   String(timestamp),
      'X-Signature':   signature,
      'Content-Type':  'application/json',
    },
    body,
  });

  const data = await res.json();
  console.log(data);
}

sendSms('255755000000', 'Hello!', 'uuid-here');
# cURL signing example (bash)
API_KEY="sk_your_api_key"
SECRET="your_api_secret"
BODY='{"to":"255755000000","message":"Hello!","sender_id":"uuid-here"}'
TS=$(date +%s)
PAYLOAD="${TS}
POST
/api/v1/sms/send
${BODY}"
SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -s -X POST https://sms.ehub.co.tz/api/v1/sms/send \
  -H "Authorization: Bearer $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Content-Type: application/json" \
  -d "$BODY"

Rate Limiting

120 requests per minute per API key.

Every response includes rate limit headers so you can track your usage in real time. When the limit is exceeded, the API returns 429 Too Many Requests with a retry_after field.

HeaderDescription
X-RateLimit-LimitMax requests per minute for your key
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets

Response Format

All responses are JSON with a consistent envelope.

Success response
{
  "success":   true,
  "message":   "SMS sent successfully",
  "data":      { /* endpoint-specific payload */ },
  "timestamp": "2026-06-05T10:00:00+03:00"
}
Error response
{
  "success":   false,
  "message":   "Invalid sender_id.",
  "timestamp": "2026-06-05T10:00:00+03:00"
}

Error Codes

Standard HTTP status codes with descriptive messages.

StatusMeaning
401Missing/invalid API key · Expired timestamp · Invalid HMAC signature · Inactive account
403Sender ID not found or not accessible · Sender ID not yet approved
409Duplicate sender ID — you have already registered this name
422Validation failed — check the errors field for details
429Rate limit exceeded — wait until the window resets (retry_after seconds)
500Internal server error — contact support if this persists

Sender IDs

Manage the alphanumeric names that appear as the SMS sender.

GET/sender-idsList sender IDs
Returns all sender IDs available to your account: your own (any status), sender IDs shared with you by an admin, and public approved sender IDs. Use the id (UUID) field when sending SMS.
GET /api/v1/sender-ids
Authorization: Bearer sk_...
X-Timestamp: 1780658993
X-Signature: abc123...
{
  "success": true,
  "data": {
    "own": [
      {
        "id":          "00420892-38bd-47b0-9a5f-ea55bef5d2d1",
        "sender_name": "MICHANGO",
        "status":      "approved",
        "purpose":     "TRANSACTIONAL",
        "is_default":  false,
        "type":        "own",
        "created_at":  "2026-01-27T14:40:26+03:00"
      }
    ],
    "shared": [],
    "public": []
  }
}
POST/sender-idsRequest sender ID
Submit a new sender ID for admin approval. The sender ID must be 4–11 alphanumeric characters. Once approved you can use it to send SMS.
Request body
FieldTypeDescription
sender_idstringrequired4–11 alphanumeric characters, will be uppercased
purposestringrequiredOne of: Notification, Transactional, OTP / Verification, Promotional, Marketing, Alerts & Reminders, Other
samplestringrequiredA realistic sample of the messages you'll send (50–250 characters)
POST /api/v1/sender-ids
Authorization: Bearer sk_...
X-Timestamp: 1780658993
X-Signature: abc123...
Content-Type: application/json

{
  "sender_id": "MYBRAND",
  "purpose":   "Promotional",
  "sample":    "Dear customer, enjoy 20% off all plans this week. Reply STOP to opt out."
}
{
  "success": true,
  "message": "Sender ID submitted for approval.",
  "data": {
    "id":          "e47c45f3-43ce-4bcc-b868-9db92840f11f",
    "sender_name": "MYBRAND",
    "status":      "pending",
    "purpose":     "Promotional",
    "created_at":  "2026-06-05T14:33:52+03:00"
  }
}

SMS

Send single or bulk SMS messages to Tanzanian phone numbers.

POST/sms/sendSend single SMS
Send a single SMS message immediately. Balance is deducted before sending and refunded automatically on failure.
Request body
FieldTypeDescription
tostringrequiredRecipient phone number (e.g. 255755000000 or 0755000000)
messagestringrequiredSMS body text, max 640 characters (4 SMS parts)
sender_iduuidrequiredUUID of an approved sender ID from GET /sender-ids
POST /api/v1/sms/send
Authorization: Bearer sk_...
X-Timestamp: 1780658993
X-Signature: abc123...
Content-Type: application/json

{
  "to":        "255755957514",
  "message":   "Your verification code is 123456",
  "sender_id": "00420892-38bd-47b0-9a5f-ea55bef5d2d1"
}
{
  "success": true,
  "message": "SMS sent successfully",
  "data": {
    "message_id": "2d853818-5529-4838-bee0-1cb012b1a136",
    "to":         "255755957514",
    "status":     "sent",
    "cost":       "25.00",
    "parts":      1,
    "created_at": "2026-06-05T13:45:33+03:00"
  }
}
POST/sms/send-bulkSend bulk SMS
Send the same message to multiple recipients in one request. Creates a campaign and dispatches messages via the queue. Supports scheduling.
Request body
FieldTypeDescription
recipientsarrayrequiredArray of phone numbers, max 1000. Duplicates are removed.
messagestringrequiredSMS body text, max 640 characters
sender_iduuidrequiredUUID of an approved sender ID
campaign_namestringoptionalLabel for this campaign, max 255 characters
scheduled_atdatetimeoptionalISO 8601 future datetime to schedule the send
POST /api/v1/sms/send-bulk
Content-Type: application/json

{
  "recipients":    ["255755000001", "255755000002", "255755000003"],
  "message":        "Flash sale! 30% off today only.",
  "sender_id":      "00420892-38bd-47b0-9a5f-ea55bef5d2d1",
  "campaign_name":  "June Flash Sale",
  "scheduled_at":   null
}
{
  "success": true,
  "message": "Bulk SMS campaign created successfully",
  "data": {
    "campaign_id":       "f84e8878-27be-4bce-aac8-98e1b3ec19a0",
    "name":              "June Flash Sale",
    "total_recipients":  3,
    "total_cost":        "75.00",
    "status":            "processing",
    "scheduled_at":      null,
    "created_at":        "2026-06-05T13:45:57+03:00",
    "messages": [        // one entry per recipient — store these to match delivery webhooks
      { "message_id": "9757991f-...-uuid", "to": "0712345678" },
      { "message_id": "a1b2c3d4-...-uuid", "to": "0713111222" },
      { "message_id": "c3d4e5f6-...-uuid", "to": "0714333444" }
    ]
  }
}
GET/sms/historySMS history
Paginated list of SMS messages sent from your account.
Query parameters
ParameterTypeDescription
statusstringoptionalpending · queued · sent · delivered · failed
campaign_idstringoptionalReturn only messages from this campaign (the id from /sms/send-bulk) — pull every message_id and recipient after a bulk send
from_datedateoptionalFilter from date (YYYY-MM-DD)
to_datedateoptionalFilter to date (YYYY-MM-DD)
limitintegeroptionalResults per page, max 100. Default: 20
pageintegeroptionalPage number. Default: 1
GET /api/v1/sms/history?status=delivered&limit=20&page=1
Authorization: Bearer sk_...
X-Timestamp: 1780658993
X-Signature: abc123...
{
  "success": true,
  "data": {
    "messages": [
      {
        "message_id":   "2d853818-5529-4838-bee0-1cb012b1a136",
        "to":           "255755957514",
        "message":      "Your code is 123456",
        "sender_id":    "MICHANGO",
        "status":       "delivered",
        "cost":         "25.00",
        "parts":        1,
        "sent_at":      "2026-06-05T13:45:35+03:00",
        "delivered_at": "2026-06-05T13:45:37+03:00",
        "created_at":   "2026-06-05T13:45:33+03:00"
      }
    ],
    "pagination": {
      "current_page": 1,
      "per_page":     20,
      "total":        142,
      "last_page":    8
    }
  }
}
GET/sms/{message_id}Message status
Get the current delivery status of a single message by its UUID.
GET /api/v1/sms/2d853818-5529-4838-bee0-1cb012b1a136
{
  "success": true,
  "data": {
    "message_id":   "2d853818-5529-4838-bee0-1cb012b1a136",
    "to":           "255755957514",
    "status":       "delivered",
    "cost":         "25.00",
    "sent_at":      "2026-06-05T13:45:35+03:00",
    "delivered_at": "2026-06-05T13:45:37+03:00",
    "error_message": null,
    "created_at":   "2026-06-05T13:45:33+03:00"
  }
}

Wallet

Check your SMS balance and transaction history.

GET/wallet/balanceGet balance
Returns your current SMS credit balance.
GET /api/v1/wallet/balance
{
  "success": true,
  "data": {
    "balance":     "0.00",
    "sms_balance": 78,
    "currency":    "TZS",
    "updated_at":  "2026-06-05T13:45:16+03:00"
  }
}
GET/wallet/transactionsTransactions
Paginated list of debit/credit transactions on your account.
Query parameters
ParameterTypeDescription
limitintegeroptionalResults per page, max 100. Default: 20
pageintegeroptionalPage number. Default: 1
GET /api/v1/wallet/transactions?limit=20&page=1
Authorization: Bearer sk_...
X-Timestamp: 1780658993
X-Signature: abc123...
{
  "success": true,
  "data": {
    "transactions": [
      {
        "id":          "txn_9c3a1d88-f401-4b2a-91c3-d7ec1e028f3b",
        "type":        "debit",
        "amount":      "25.00",
        "description": "SMS to 255755957514",
        "balance":     "1950.00",
        "created_at":  "2026-06-05T13:45:33+03:00"
      },
      {
        "id":          "txn_2aa7e1b3-0c43-4d19-b98c-0fa3c9b23ef0",
        "type":        "credit",
        "amount":      "5000.00",
        "description": "Wallet top-up via Snippe",
        "balance":     "1975.00",
        "created_at":  "2026-06-04T09:12:05+03:00"
      }
    ],
    "pagination": {
      "current_page": 1,
      "per_page":     20,
      "total":        47,
      "last_page":    3
    }
  }
}

Plans & Payments

View pricing tiers, initiate top-ups, and check payment status.

GET/plansList pricing plans
Returns all active pricing tiers with SMS quantity ranges and price per SMS.
GET /api/v1/plans
{
  "success": true,
  "data": {
    "plans": [
      {
        "name":         "Starter",
        "min_sms":      1,
        "max_sms":      999,
        "price_per_sms": 16,
        "currency":     "TZS"
      },
      {
        "name":         "Growth",
        "min_sms":      1000,
        "max_sms":      4999,
        "price_per_sms": 14,
        "currency":     "TZS"
      }
    ]
  }
}
POST/payments/initiateInitiate top-up
Initiate a mobile money payment to top up your SMS credits. A push notification will be sent to the provided phone number. Rate limited to 30 requests per minute. Total top-ups from this endpoint are capped at TZS 150,000 per user per calendar day (EAT); requests that would exceed the cap return 422.
Request body
ParameterTypeDescription
sms_quantityintegerrequiredNumber of SMS credits to purchase
phone_numberstringrequiredMobile money number (e.g. 0755957514)
idempotency_keystringoptionalUnique key (max 64 chars) to prevent duplicate charges
POST /api/v1/payments/initiate

{
  "sms_quantity":     100,
  "phone_number":    "0755957514",
  "idempotency_key": "pay_abc123"
}
{
  "success": true,
  "message": "Payment initiated. Check your phone.",
  "data": {
    "order_reference": "ORD260721092015ABCDE",
    "amount":          1600,
    "sms_quantity":    100,
    "price_per_sms":   16,
    "currency":        "TZS",
    "gateway":         "snippe"
  }
}
{
  "success": false,
  "message": "Daily API top-up limit of TZS 150,000 exceeded. You have TZS 20,000 remaining today.",
  "errors": {
    "daily_limit_tzs":     150000,
    "spent_today_tzs":     130000,
    "remaining_today_tzs": 20000
  }
}
GET/payments/{order_reference}/statusPayment status
Check the status of a payment. Poll this endpoint after initiating a payment to track completion.
Possible statuses
StatusDescription
pendingPayment created, waiting for gateway
processingPush sent to phone, waiting for confirmation
completedPayment successful, SMS credits added
failedPayment failed or expired
cancelledPayment cancelled by user
GET /api/v1/payments/ORD260721092015ABCDE/status
{
  "success": true,
  "data": {
    "order_reference": "ORD260721092015ABCDE",
    "amount":          1600,
    "sms_quantity":    100,
    "status":          "completed",
    "currency":        "TZS",
    "completed_at":    "2026-07-21T09:20:45+03:00",
    "created_at":      "2026-07-21T09:20:15+03:00"
  }
}
GET/paymentsPurchase history
Paginated list of your top-ups. Optional filters: status, from_date, to_date, limit (max 100), page.
{
  "data": {
    "payments": [ {
      "order_reference": "ORD...",
      "amount":          3000,
      "sms_quantity":    100,
      "price_per_sms":   30,
      "status":         "completed",
      "gateway":        "clickpesa",
      "completed_at":   "2026-08-30T09:15:00+03:00"
    } ],
    "pagination": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
  }
}

Reseller & Sub-accounts

Give each of your own customers an independent SMS balance under one eHub account.

Create an API key per customer, then allocate SMS credits to it from your main wallet. A metered key spends only its own balance, so your customers are billed and rate-limited independently — and can never spend each other's or your credits.

Master vs customer keys

Only a master key may manage keys or move wallet credits (the endpoints below). Designate exactly one master key in the dashboard (API Keys → Set as master). Every other key — including ones created via the API for your customers — is non-master and cannot create keys or allocate credit. Calling these endpoints with a non-master key returns 403.

How allocation works

Allocating moves credits wallet → key. If your wallet is short, we fund what's available now and record the rest as pending (visible to the customer but not spendable). Pending settles automatically, oldest-first, the next time you top up your wallet.

POST/api-keysCreate API key · master only
Create a customer API key. The key and secret are returned once — store them now; the key is stored only as a hash and cannot be retrieved again.
Request body
FieldTypeDescription
namestringrequiredLabel for the key (e.g. the customer's name)
meteredbooleanoptionalBill this key's own balance. Auto-enabled on first allocation.
expires_in_daysintegeroptionalAuto-expire after N days (1–3650)
POST /api/v1/api-keys
Authorization: Bearer sk_master...

{
  "name": "George",
  "metered": true
}
{
  "success": true,
  "message": "API key created. Store the key and secret now — they will not be shown again.",
  "data": {
    "id":      42,
    "name":    "George",
    "key":     "sk_9f8c1e2a...",
    "secret":  "a1b2c3...",
    "metered": true
  }
}
GET/api-keysList API keys · master only
List your keys with status and (for metered keys) balances. Secrets and raw keys are never returned.
{
  "data": { "keys": [ {
    "id": 42, "name": "George",
    "is_active": true, "metered": true,
    "sms_balance": 3000, "sms_pending": 2000
  } ] }
}
DELETE/api-keys/{id}Deactivate key · master only
Deactivate one of your keys by its id (from the list endpoint). Immediately blocks that customer.
Response
{
  "success": true,
  "message": "API key deactivated.",
  "data": { "id": 42, "is_active": false }
}
POST/allocationsAllocate credits · master only
Move SMS credits from your wallet to a customer key. Optionally SMS the customer a confirmation. A short wallet balance partially funds now and records the rest as pending (not an error).
Request body
FieldTypeDescription
keystringrequiredThe customer key's plaintext value (sk_…)
smsintegerrequiredCredits to allocate
sender_iduuidoptionalSender UUID for the confirmation SMS (must be one you can use)
customer_namestringoptionalPersonalizes the confirmation
customer_msisdnstringoptionalWhere to send the confirmation SMS
messagestringoptionalCustom template: {customer_name} {sender} {sms} {available} {pending}
idempotency_keystringoptionalSafe retries — the same key returns the original result
POST /api/v1/allocations
Authorization: Bearer sk_master...

{
  "key":             "sk_9f8c1e2a...",
  "sms":             5000,
  "sender_id":       "e47c45f3-...-uuid",
  "customer_name":   "George",
  "customer_msisdn": "0712345678"
}
{
  "success": true,
  "data": {
    "funded":      3000,
    "pending":     2000,
    "key_usable":  3000,
    "key_pending": 2000,
    "notice":     { "message_id": "...", "status": "sent" }
  }
}
POST/allocations/deallocateDeallocate · master only
Return usable credits from a customer key back to your wallet.
Request body
FieldTypeDescription
keystringrequiredThe customer key's plaintext value (sk_…)
smsintegerrequiredCredits to return to your wallet (capped at the key's usable balance)
Response
{
  "success": true,
  "data": {
    "returned":              1500,
    "key_usable":            2500,
    "reseller_sms_balance":  2500
  }
}
GET/allocations/keysMetered keys & balances · master only
List your metered customer keys with usable / reserved / pending balances. You can also check a single key via GET /wallet/balance?key=sk_….
Response
{
  "data": {
    "keys": [ {
      "name":         "George",
      "sms_balance":  3000,   // usable now
      "sms_reserved": 0,
      "sms_pending":  2000,   // allocated, not yet funded
      "is_active":    true,
      "last_used_at": "2026-08-30T09:15:00+03:00"
    } ]
  }
}
GET/allocationsAllocation history · master only
Paginated history of your allocations (key, customer, funded, pending, date). Optional limit (max 100) and page.
Response
{
  "data": {
    "allocations": [ {
      "key":           "George",
      "customer_name": "George",
      "funded":        3000,
      "pending":       2000,
      "created_at":    "2026-08-30T09:15:00+03:00"
    } ],
    "pagination": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
  }
}

Customers

Onboard your own customers as headless eHub accounts — they use the API through your system and never log in.

POST/customersRegister customer · master only
Creates a real user (own wallet & pricing) with a temporary password that is never returned, and issues an API key for them. The key + secret are shown once — hand them to your customer or use them from your system.
Request body
FieldTypeDescription
namestringrequiredCustomer's name
emailstringrequiredUnique email (identifier; no login email is sent)
phone_numberstringrequiredTanzania format (0…/255…/+255…)
key_namestringoptionalLabel for the issued key
POST /api/v1/customers
Authorization: Bearer sk_master...

{
  "name":         "Acme Ltd",
  "email":        "ops@acme.co.tz",
  "phone_number": "0712345678"
}
{
  "success": true,
  "data": {
    "customer_id": 128,
    "name":        "Acme Ltd",
    "email":       "ops@acme.co.tz",
    "api_key": {
      "id":     42,
      "key":    "sk_9f8c1e2a...",
      "secret": "a1b2c3..."
    }
  }
}
GET/customersList customers · master only
Paginated list of the customers you've provisioned. Optional limit (max 100) and page.
Response
{
  "data": {
    "customers": [ {
      "customer_id":  128,
      "name":         "Acme Ltd",
      "email":        "ops@acme.co.tz",
      "phone_number": "0712345678",
      "is_active":    true,
      "api_keys":     1,
      "created_at":   "2026-08-30T09:15:00+03:00"
    } ],
    "pagination": { "total": 1, "per_page": 20, "current_page": 1, "last_page": 1 }
  }
}

Delivery Report Webhook

Get a real-time callback when your messages are delivered or fail.

Set an HTTPS endpoint in the dashboard (Developer → Webhook). Whenever one of your messages reaches a terminal state, we POST a JSON payload to it. Each POST carries an X-eHub-Signature header — the HMAC-SHA256 of the raw request body, keyed with your webhook secret. Recompute it and compare (constant-time) to verify the call really came from eHub.

Events

EventWhen
sms.deliveredThe handset confirmed receipt.
sms.failedThe network gave up; error says why.
webhook.testSent from the dashboard's "Send a test event" only.

Headers on every request

HeaderValue
X-eHub-EventThe event name, for routing before you parse the body.
X-eHub-SignatureHMAC-SHA256 of the raw body, hex, keyed with your webhook secret.
Content-Typeapplication/json

Payload

{
  "event":        "sms.delivered",   // or "sms.failed"
  "message_id":   "9757991f-...-uuid",
  "to":           "+255712345678",
  "status":       "delivered",
  "sender_id":    "MYBRAND",
  "error":        null,
  "sent_at":      "2026-08-30T09:14:50+03:00",
  "delivered_at": "2026-08-30T09:15:00+03:00",
  "failed_at":    null,
  "timestamp":    1788069700
}

Verify the signature

$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $yourWebhookSecret);
if (! hash_equals($expected, $_SERVER['HTTP_X_EHUB_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
import hmac, hashlib

expected = hmac.new(
    your_webhook_secret.encode(), raw_body, hashlib.sha256
).hexdigest()

sig = request.headers.get('X-eHub-Signature', '')
if not hmac.compare_digest(expected, sig):
    abort(401)
const crypto = require('crypto');

const expected = crypto
  .createHmac('sha256', yourWebhookSecret)
  .update(rawBody)            // the raw request body
  .digest('hex');

const sig = req.get('X-eHub-Signature') || '';
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
  return res.sendStatus(401);
}
body, _ := io.ReadAll(r.Body)

mac := hmac.New(sha256.New, []byte(yourWebhookSecret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))

if !hmac.Equal([]byte(expected), []byte(r.Header.Get("X-eHub-Signature"))) {
    w.WriteHeader(http.StatusUnauthorized)
    return
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = HexFormat.of().formatHex(
    mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8)));

String sig = request.getHeader("X-eHub-Signature");
if (!MessageDigest.isEqual(expected.getBytes(), sig.getBytes())) {
    response.setStatus(401);
    return;
}
require 'openssl'

expected = OpenSSL::HMAC.hexdigest('SHA256', your_webhook_secret, raw_body)

sig = request.env['HTTP_X_EHUB_SIGNATURE'].to_s
unless Rack::Utils.secure_compare(expected, sig)
  halt 401
end

Retries

Respond with any 2xx within 10 seconds to acknowledge. Anything else is retried after 30 s, 2 min and 10 min — four attempts in total — then dropped. Retries mean your handler should be idempotent on message_id.