Developer Documentation

TeraMouv SMS Gateway
API Reference

A stable, secure JSON/REST interface for sending and receiving SMS messages across Burundi. This guide covers everything you need to integrate — from obtaining your first API key to handling edge cases in production.

https://api.sms.teramouv.com/api/v1

Overview

The TeraMouv API Gateway is a thin, stable REST layer that lets your application send A2P SMS messages and receive P2A replies without ever touching the underlying messaging infrastructure directly.

Protocol basics

PropertyValue
ProtocolHTTPS only — https://api.sms.teramouv.com. No HTTP fallback.
Content-Typeapplication/json on every request body and every response.
API Versionv1, embedded in the URL path (/api/v1/…). Breaking changes ship under a new path prefix; v1 stays stable.
AuthenticationSingle X-API-Key header on all protected requests.
CoverageBurundi numbers only. Format: 257XXXXXXXX — country code 257 + 8 local digits = 11 digits total. Example: 25700000000.
Message limit160 characters hard cap. No multipart/concatenated SMS.

Quick Start

Three calls to go from zero to a delivered SMS. Complete the steps below to send your first message.

  1. Obtain an API key

    Call POST /auth/login with your credentials. Save the returned api_key — you'll send it as the X-API-Key header on every subsequent request.

    cURL
    curl -X POST https://api.sms.teramouv.com/api/v1/auth/login \
      -H "Content-Type: application/json" \
      -d '{"username":"your_username","password":"your_password"}'
    ⚠️

    Production systems: do not call /auth/login from every process. Ask your TeraMouv administrator for a permanently provisioned API key instead. See API Key Types below.

  2. Send an SMS

    Use the key from Step 1 in the X-API-Key header. A 200 response means the message was accepted — not yet delivered. Save the message_id for status polling.

    cURL
    curl -X POST https://api.sms.teramouv.com/api/v1/sms/send \
      -H "Content-Type: application/json" \
      -H "X-API-Key: sk_live_..." \
      -d '{"to":"25700000000","message":"Hello from TeraMouv","from":"TeraMouv"}'
  3. Poll for delivery status

    Use the message_id from Step 2 to check whether the message reached the handset. The status moves from submittedpendingsentdelivered (or failed).

    cURL
    curl https://api.sms.teramouv.com/api/v1/sms/status/msg_901991 \
      -H "X-API-Key: sk_live_..."

API Key Types

There are two ways to obtain an API key. Understanding the difference is critical for production deployments.

TypeHow obtainedBehaviour on re-loginRecommended for
Session key Call POST /auth/login Any previous session key for the same account is immediately revoked One-off scripts, manual testing, single-process applications
Permanent key Provisioned directly by a TeraMouv administrator Not affected by /auth/login calls Production servers, multi-process / multi-host deployments, server-to-server integrations
🚫

Multi-process pitfall: if two of your own services each call /auth/login with the same credentials, the second login silently invalidates the first service's key. That service will start receiving 401 UNAUTHORIZED on every request without any warning. Use a permanent key — contact your administrator.

IP Whitelisting

A key can optionally be restricted to a list of source IP addresses. Requests from an unlisted IP are rejected with 403 IP_NOT_ALLOWED regardless of how valid the key is. Contact your administrator to view or update your key's IP whitelist.


Authentication

All protected endpoints require the X-API-Key header. The key is checked before any business logic runs — an invalid or missing key always returns 401 immediately.

POST /auth/login No auth required Obtain an API key

Exchange your username and password for an API key. The key does not expire — it stays valid until you call /auth/logout or log in again with the same account (which revokes the previous key).

Request body

FieldTypeRequiredDescription
usernameREQUIRED stringYes Your account username, as provided by TeraMouv.
passwordREQUIRED stringYes Your account password.

Example request

cURL
curl -X POST https://api.sms.teramouv.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"your_username","password":"your_password"}'
JavaScript (fetch)
const res = await fetch('https://api.sms.teramouv.com/api/v1/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    username: 'your_username',
    password: 'your_password'
  })
});
const { api_key, user } = await res.json();
// Store api_key securely — use it in X-API-Key on subsequent requests
PHP (cURL)
$ch = curl_init('https://api.sms.teramouv.com/api/v1/auth/login');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS     => json_encode([
        'username' => 'your_username',
        'password' => 'your_password'
    ]),
]);
$data = json_decode(curl_exec($ch), true);
$apiKey = $data['api_key'];
Python (requests)
import requests

resp = requests.post(
    'https://api.sms.teramouv.com/api/v1/auth/login',
    json={'username': 'your_username', 'password': 'your_password'}
)
api_key = resp.json()['api_key']

Response 200

JSON
{
  "success": true,
  "api_key": "sk_live_abc123...",
  "user": {
    "id": 2,
    "username": "your_username",
    "email": "you@example.com"
  }
}

Error responses

HTTPError codeCause
422VALIDATION_FAILEDMissing or malformed fields in the request body
401INVALID_CREDENTIALSUsername or password is incorrect
POST /auth/logout X-API-Key required Revoke the current key

Immediately revokes the API key sent in the X-API-Key header. The key cannot be reused after this call. Safe to retry freely (subsequent calls with the now-invalid key simply return 401).

Request

No request body needed — the key to revoke is identified by the X-API-Key header itself.

Response 200

JSON
{ "success": true, "message": "Logged out successfully." }
ℹ️

Call this endpoint when you want to explicitly invalidate a session key. For permanently provisioned keys, contact your administrator for revocation instead.


SMS

Send outbound A2P messages, check delivery receipts, and retrieve inbound P2A messages.

POST /sms/send X-API-Key required Submit one SMS

Submits a single SMS message for delivery. A 200 response means the message was accepted into the send queue — it does not confirm delivery to the handset. Delivery is asynchronous; poll /sms/status/{message_id} for the final outcome.

ℹ️

One message, one destination per call. There is no batch/bulk send endpoint. To send to multiple recipients, issue one /sms/send call per destination number.

Request fields

FieldTypeRequiredValidation & notes
toREQUIRED stringYes Destination number in Burundi format: 257XXXXXXXX or +257XXXXXXXX. Country code 257 followed by exactly 8 local digits — 11 digits total (e.g. 25700000000). The leading + is stripped automatically. Numbers with fewer or more than 8 local digits, or any other country code, are rejected with 422 VALIDATION_FAILED.
messageREQUIRED stringYes Message text: 1–160 characters. Messages over 160 characters are rejected outright — there is no automatic splitting into multiple segments.
fromOPTIONAL stringNo Sender ID (alphanumeric or numeric). Must be a value your account is authorised to use. An unrecognised sender ID is rejected by the upstream carrier — not by this gateway — so you will not receive a 422 for an invalid from value; the message will fail further downstream.
scheduleOPTIONAL stringNo Future send time. Accepts ISO 8601 (2026-06-15T10:00:00) or MySQL datetime (2026-06-15 10:00:00) formats. Omit to send immediately.

Example requests

cURL — immediate send
curl -X POST https://api.sms.teramouv.com/api/v1/sms/send \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_..." \
  -d '{
    "to": "25700000000",
    "message": "Your OTP code is 482910. Valid for 5 minutes.",
    "from": "TeraMouv"
  }'
cURL — scheduled send
curl -X POST https://api.sms.teramouv.com/api/v1/sms/send \
  -H "Content-Type: application/json" \
  -H "X-API-Key: sk_live_..." \
  -d '{
    "to": "25700000000",
    "message": "Your appointment is tomorrow at 09:00.",
    "from": "TeraMouv",
    "schedule": "2026-06-20T08:00:00"
  }'
JavaScript
async function sendSMS(to, message, from) {
  const res = await fetch('https://api.sms.teramouv.com/api/v1/sms/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.TERAMOUV_API_KEY
    },
    body: JSON.stringify({ to, message, from })
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`SMS failed: ${err.error} — ${err.message}`);
  }

  return await res.json(); // { success, message_id, status, ... }
}
PHP
function sendSMS(string $to, string $message, string $from): array {
    $ch = curl_init('https://api.sms.teramouv.com/api/v1/sms/send');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'X-API-Key: ' . getenv('TERAMOUV_API_KEY')
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'to'      => $to,
            'message' => $message,
            'from'    => $from,
        ]),
    ]);
    $data = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $data;
}
Python
import os, requests

def send_sms(to: str, message: str, sender_id: str = None) -> dict:
    payload = {"to": to, "message": message}
    if sender_id:
        payload["from"] = sender_id

    resp = requests.post(
        "https://api.sms.teramouv.com/api/v1/sms/send",
        json=payload,
        headers={"X-API-Key": os.environ["TERAMOUV_API_KEY"]},
        timeout=10
    )
    resp.raise_for_status()
    return resp.json()  # { "success": True, "message_id": "msg_...", ... }

Response 200

JSON
{
  "success": true,
  "message_id": "msg_901991",      // save this — use it to poll /sms/status
  "status": "submitted",
  "destination": "25700000000",
  "credits_deducted": 1,             // always 1 per accepted message
  "timestamp": "2026-06-03T16:49:42+02:00"
}

Error responses

HTTPError codeYour problem?Action
401UNAUTHORIZED✅ YesCheck your X-API-Key header. The key may be missing, revoked, or mistyped.
403IP_NOT_ALLOWED✅ YesYour source IP is not in the whitelist for this key. Contact your administrator.
422VALIDATION_FAILED✅ YesInvalid to format, empty message, or message over 160 chars. See the errors object in the response.
402INSUFFICIENT_CREDIT✅ YesYour account has run out of SMS credits. Contact TeraMouv to top up.
429RATE_LIMIT_EXCEEDED✅ YesSlow down. Wait Retry-After seconds before the next request.
502SEND_FAILED❌ GatewayThe upstream carrier rejected the send as a structured error. Contact support if persistent.
400SEND_MISSING_FIELDS❌ GatewayRare — the gateway's upstream reported empty fields despite passing local validation.
401AUTH_FAILED / TOKEN_UNAVAILABLE / TOKEN_DISABLED❌ GatewayThe gateway's own connection to the upstream is misconfigured. Not your API key. Contact support.
403TOKEN_IP_BLOCKED❌ GatewayThe gateway server's IP is blocked upstream. TeraMouv operational issue. Contact support.
500INTERNAL_SERVER_ERROR❌ GatewayUpstream unreachable or unparseable. Do not blindly retry — the message may already be queued. See Retries & Idempotency.
GET /sms/status/{message_id} X-API-Key required Poll delivery status

Returns the last known delivery status for a message. This endpoint always succeeds (unless the message_id is not found) — if the upstream is temporarily unreachable, it falls back to the locally cached record rather than returning an error.

Message status lifecycle

submitted
pending
sent
delivered
/
failed
StatusMeaning
submittedAccepted by this gateway and forwarded to the upstream. No further status received yet.
pendingIn the upstream queue, not yet dispatched to the telco.
sentDispatched to the carrier SMSC, awaiting a delivery receipt (DLR).
deliveredA delivery receipt was received from the telco confirming handset delivery. Terminal — poll no further.
failedThe telco reported delivery as failed. Terminal — do not retry without investigating the cause.

Path parameter

ParameterTypeDescription
message_idstringThe message_id value returned by a previous /sms/send call. Example: msg_901991.

Response 200

JSON
{
  "message_id": "msg_901991",
  "status": "delivered",
  "destination": "25700000000",
  "submitted_at": "2026-06-03 16:49:42",  // YYYY-MM-DD HH:MM:SS, no timezone
  "updated_at": "2026-06-03 16:49:47",     // null if no live DLR check succeeded
  "error_code": "0"
}
⚠️

Timezone note: submitted_at and updated_at are plain YYYY-MM-DD HH:MM:SS strings passed through as-is — unlike the ISO 8601 timestamp field on other endpoints, these carry no timezone offset. Treat them as server-local time.

Error responses

HTTPError codeCause
404NOT_FOUNDThe message_id does not exist, or belongs to a different API key.

Polling recommendation

Poll at a reasonable interval — every 5–10 seconds is appropriate. Stop polling once the status reaches delivered or failed (both are terminal states). There is no push notification / webhook mechanism.

GET /sms/inbox X-API-Key required Retrieve incoming P2A messages

Returns a paginated list of inbound (person-to-application) SMS messages received on your account. Poll this endpoint to detect replies.

Query parameters

ParameterDefaultMaxDescription
limit10100Number of messages to return per page.
offset0Zero-based pagination offset. Increment by limit to fetch the next page.
⚠️

Pagination caveat: the total field in the response is the count of rows returned in this page only — it is not the grand total of all inbox messages. You cannot use it to calculate "how many pages remain." Continue paginating until a page returns fewer messages than your limit.

Response 200

JSON
{
  "success": true,
  "total": 5,              // count of rows in THIS page, not all-time total
  "limit": 10,
  "offset": 0,
  "messages": [
    {
      "id": "1",
      "from": "25700000000",
      "message": "STOP",
      "received_at": "2026-06-03T09:55:00+02:00"
    }
  ]
}

Error responses

HTTPError codeCause
401AUTH_FAILED / TOKEN_UNAVAILABLE / TOKEN_DISABLEDGateway-side upstream connection issue — not your API key. Contact support.
403TOKEN_IP_BLOCKEDGateway server IP blocked on the upstream side. Contact support.
500INTERNAL_SERVER_ERRORUpstream unreachable or returned unparseable response.

Account

Check your credit balance and retrieve your full account profile from the gateway.

GET /account/credit X-API-Key required Check SMS credit balance

Returns the current credit balance on your account. Credits are denominated in SMS messages (1 credit = 1 SMS segment). Monitor this to avoid hitting 402 INSUFFICIENT_CREDIT on sends.

Response 200

JSON
{
  "success": true,
  "balance": 4989564,
  "currency": "SMS",  // always the literal string "SMS", not a monetary currency
  "timestamp": "2026-06-03T16:49:42+02:00"
}
GET /account/info X-API-Key required Full account profile

Returns the full account record associated with your API key, including balance and message history pointers.

Response 200

JSON
{
  "success": true,
  "data": {
    "username": "your_username",
    "name": "Your Company",
    "email": "you@example.com",
    "mobile": "",
    "balance": 4989564,
    "last_outgoing_id": "901991",  // highest sent message ID on record
    "last_incoming_id": "0",       // highest received message ID
    "last_inbox_id": "0"           // highest two-way inbox message ID
  },
  "timestamp": "2026-06-03T16:49:42+02:00"
}

Field descriptions

FieldDescription
last_outgoing_idThe highest internal log ID of an outgoing message on this account. Useful for detecting new outbound activity.
last_incoming_idThe highest ID of an incoming (received) message.
last_inbox_idThe highest ID of a two-way inbox-style message.
GET /health No auth required Gateway health check

Public endpoint — no API key required. Returns 200 when the gateway process is running. Safe to poll as frequently as your load balancer or uptime monitor requires.

⚠️

Scope: this endpoint only confirms the gateway process itself is alive. It does not check upstream connectivity, Redis, or database availability. A 200 here does not guarantee that /sms/send will succeed.

Response 200

JSON
{
  "status": "ok",
  "service": "TeraMouv API Gateway",
  "timestamp": "2026-06-03T16:49:42+02:00"
}

Request Validation

Every POST / PUT / PATCH request is validated before your API key is even checked. A bad request always returns 400, never 401.

RuleWhat happens if violated
Content-Type: application/json must be present400 BAD_REQUEST — before auth check
Body must be valid JSON (if non-empty)400 BAD_REQUEST — before auth check
Empty bodyTreated as {} — required fields then caught by 422 VALIDATION_FAILED
Required fields missing or invalid422 VALIDATION_FAILED with per-field details in errors
422 Validation error shape
{
  "success": false,
  "error": "VALIDATION_FAILED",
  "errors": {
    "to": ["The to format is invalid."],
    "message": ["The message field is required."]
  }
}

Rate Limiting

Each API key has its own rate limit window. The default is 1,000 requests per 60-second window, configurable per key by an administrator.

Every protected endpoint response includes these headers so you can track consumption in real time:

Response headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 995

When the limit is exceeded, the gateway responds with:

429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Retry-After: 60

{
  "success": false,
  "error": "RATE_LIMIT_EXCEEDED",
  "message": "Rate limit of 1000 requests per minute exceeded."
}
ℹ️

Wait the number of seconds specified in the Retry-After header before retrying. If your integration requires a higher limit, contact your TeraMouv administrator to have your key's threshold raised.


Retries & Idempotency

/sms/send is not idempotent. If you retry a send that timed out or returned 500, you may end up delivering the same message twice.

🚫

Do not blindly retry /sms/send after a timeout or 500 INTERNAL_SERVER_ERROR. The original message may already be queued upstream even though the response never reached you.

Recovery procedure

  1. You received a message_id before the failure

    Call GET /sms/status/{message_id} first. If the status is submitted or higher, the message is already in the queue — do not resend.

  2. You never received a message_id

    The request failed before a response was returned. There is currently no lookup mechanism for this case. Wait briefly and check your own application logs. If duplicate sends are a serious concern for your use case, contact TeraMouv support.

Safe to retry freely

All GET requests and /auth/logout are idempotent and can be retried without any risk of side effects.


Error Handling

All error responses share a consistent shape. The error field is the machine-readable code; message is a human-readable description.

Standard error shape

JSON
{
  "success": false,
  "error": "ERROR_CODE",
  "message": "Human-readable description of what went wrong."
}

Validation error shape (extra errors object)

JSON
{
  "success": false,
  "error": "VALIDATION_FAILED",
  "errors": {
    "to": ["The to format is invalid."],
    "message": ["The message field is required."]
  }
}

Complete error code reference

HTTPError codeYour integration?When it occurs
400BAD_REQUEST✅ YesNon-JSON body, malformed JSON, or wrong / missing Content-Type.
400SEND_MISSING_FIELDS❌ GatewayUpstream reports destination or message empty. Rare — gateway validates first.
401UNAUTHORIZED✅ YesMissing, invalid, or revoked API key.
401INVALID_CREDENTIALS✅ YesWrong username or password on /auth/login.
401AUTH_FAILED❌ GatewayGateway's own upstream authentication failed — not your API key.
401TOKEN_UNAVAILABLE❌ GatewayGateway's upstream webservices token not available — not your API key.
401TOKEN_DISABLED❌ GatewayGateway's upstream webservices token disabled — not your API key.
402INSUFFICIENT_CREDIT✅ YesNot enough SMS credit on the account. Top up with your administrator.
403IP_NOT_ALLOWED✅ YesRequest source IP not in this key's IP whitelist.
403TOKEN_IP_BLOCKED❌ GatewayGateway server IP blocked on the upstream side — TeraMouv operational issue.
404NOT_FOUND✅ Yesmessage_id not found or belongs to a different key.
405METHOD_NOT_ALLOWED✅ YesWrong HTTP method for the endpoint (e.g. GET on /sms/send).
422VALIDATION_FAILED✅ YesInvalid or missing fields — see per-field errors object.
429RATE_LIMIT_EXCEEDED✅ YesRate limit window exceeded. Wait Retry-After seconds.
500INTERNAL_SERVER_ERROR❌ GatewayUnexpected gateway error or upstream unreachable / unparseable.
502SEND_FAILED❌ GatewayUpstream rejected the send as a structured failure.
ℹ️

Any error marked ❌ Gateway indicates a problem with the gateway's own upstream connection — retrying from your end will not resolve it. Contact TeraMouv support if these errors persist.


Limits & Constraints

Hard boundaries enforced by this API version. These are not configurable per key.

ConstraintValueNotes
Country coverage Burundi only to must be 257XXXXXXXX or +257XXXXXXXX — country code 257 followed by exactly 8 local digits (11 digits total). No other country code is accepted.
Message length 1–160 characters Hard cap. Messages over 160 chars are rejected. There is no automatic splitting into multiple segments (no concatenated SMS).
Batch sending Not supported One message, one destination per /sms/send call. Loop on your side for multiple recipients.
Webhooks / push Not supported No push delivery receipts or inbound notifications. Poll /sms/status and /sms/inbox instead.
Idempotency key Not supported /sms/send has no deduplication. See Retries & Idempotency before retrying on errors.
Rate limit (default) 1,000 req / 60 sec Per API key. Configurable by administrator on request.

Security Headers

Every response — including errors — carries these hardening headers. They don't affect server-to-server clients but are worth knowing when debugging via a browser or proxy.

HTTP Response Headers
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'