Messages API
Full reference for message send, status, and list endpoints.
POST /api/messages/send
Send a single SMS message.
Auth: Required
Request body
{
"to": "+256712345678",
"text": "Your OTP is 482910.",
"sender_id": "MyApp",
"route": "ESMS_UG"
}| Field | Type | Required | Description |
|---|---|---|---|
to | string | ✅ | Recipient phone in E.164 format |
text | string | ✅ | Message content |
sender_id | string | - | Approved sender ID. Defaults to route default |
route | string | - | Route code e.g. ESMS_UG. Auto-detected if omitted |
schedule_mode | string | - | "now" (default) or "scheduled" |
scheduled_at | string | If scheduled | ISO 8601 UTC datetime, at least 5 minutes in the future |
Response 200
{
"id": "msg-a1b2c3d4",
"status": "submitted",
"segments": 1,
"cost": 1.2022,
"cost_currency": "KES",
"route_cost": 35.00,
"route_currency": "UGX",
"route": "ESMS_UG",
"balance_after": 1965.00,
"scheduled_at": null
}When schedule_mode is "scheduled", status will be "scheduled" and scheduled_at will contain the dispatch time. cost / cost_currency is the wallet deduction. route_cost / route_currency is the carrier's native price.
Errors
| Status | Detail |
|---|---|
| 400 | Phone cannot be parsed or is missing country code |
| 400 | No active route for this country |
| 400 | scheduled_at missing, less than 5 minutes in the future, or more than 7 days out |
| 403 | Sender ID not found, not approved, or not approved for this country |
| 422 | Wallet balance too low |
POST /api/messages/bulk-precheck
Calculate cost of a bulk send before committing.
Auth: Required
Request body
{
"contact_list_ids": [1, 2],
"text": "Hi {{name}}, this is a test.",
"sender_id": "MyApp"
}Response 200
{
"total_contacts": 250,
"total_segments": 350,
"total_cost": 1.88,
"wallet_currency": "USD",
"wallet_balance": 5000.00,
"balance_after": 4998.12,
"sufficient_balance": true,
"countries": 2,
"breakdown": [
{
"country_code": "UG",
"country_name": "Uganda",
"currency": "UGX",
"route_code": "ESMS_UG",
"contacts": 150,
"segments": 200,
"route_cost": 7000.00,
"wallet_cost": 1.88,
"price_per_segment": 35.00
}
],
"unroutable_count": 1,
"unroutable": ["+000000000"]
}POST /api/messages/send-bulk
Send a bulk SMS campaign.
Auth: Required
Request body
{
"contact_list_ids": [1, 2],
"text": "Hi {{name}}, your message here.",
"sender_id": "MyApp",
"schedule_mode": "now",
"scheduled_at": null,
"drip_rate": null
}| Field | Type | Required | Description |
|---|---|---|---|
contact_list_ids | array | ✅ | List IDs to send to |
text | string | ✅ | Message text with optional {{variables}} |
sender_id | string | - | Approved sender ID |
route | string | - | Force a specific route |
schedule_mode | string | - | now (default), scheduled, drip |
scheduled_at | string | If scheduled | ISO 8601 UTC datetime |
drip_rate | integer | If drip | Messages per minute |
Response 200
{
"batch_id": "batch-uuid",
"total_recipients": 250,
"estimated_cost": 1.88,
"status": "sending"
}GET /api/messages
List messages with pagination and optional status filter.
Auth: Required
Query params
| Param | Default | Description |
|---|---|---|
page | 0 | 0-indexed page |
limit | 20 | 1–100 |
status | - | Filter: queued, submitted, scheduled, dripping, delivered, failed |
Response 200
{
"messages": [
{
"id": "msg-a1b2c3d4",
"phone": "+256712345678",
"text": "Hello",
"sender_id": "eSMSAfrica",
"route": "ESMS_UG",
"country": "UG",
"segments": 1,
"cost": 35.00,
"currency": "UGX",
"status": "delivered",
"error_code": null,
"retry_count": 0,
"created_at": "2026-05-02T12:00:00Z",
"delivered_at": "2026-05-02T12:00:05Z"
}
],
"total": 150,
"page": 0,
"limit": 20
}GET /api/messages/:message_id
Get full message details including the event timeline.
Auth: Required
Response 200
{
"id": "msg-a1b2c3d4",
"phone": "+256712345678",
"text": "Hello",
"sender_id": "eSMSAfrica",
"route": "ESMS_UG",
"country": "UG",
"segments": 1,
"cost": 35.00,
"currency": "UGX",
"status": "delivered",
"delivered_at": "2026-05-02T12:00:05Z",
"timeline": [
{
"event": "accepted",
"status": "queued",
"detail": "API request received",
"at": "2026-05-02T12:00:00Z",
"metadata": { "source": "api_key" }
},
{
"event": "submitted",
"status": "submitted",
"detail": "Gateway accepted",
"at": "2026-05-02T12:00:01Z",
"metadata": { "gateway_message_id": "0005e27f-6c0c" }
},
{
"event": "delivered",
"status": "delivered",
"detail": "DLR: DELIVRD",
"at": "2026-05-02T12:00:05Z"
}
]
}POST /api/messages/:message_id/retry
Retry a failed message.
Auth: Required
Only works on messages with status: "failed". Returns 409 if the message is not in a retryable state.
Response 200
{
"id": "msg-a1b2c3d4",
"status": "retrying",
"retry_count": 1
}GET /api/messages/stats
Dashboard KPIs - counts for today vs. yesterday, delivery rate, balance.
Auth: Required
Response 200
{
"sent_today": 250,
"sent_delta_pct": 5.5,
"delivery_rate": 94.2,
"failed_today": 2,
"failed_delta": -1,
"balance": 5000.50,
"currency": "USD",
"period_total": 5000,
"period_days": 14
}GET /api/messages/volume
Daily message volume for the last N days.
Auth: Required
Query params
| Param | Default | Description |
|---|---|---|
days | 14 | 1–90 |
Response 200
[
{
"date": "2026-05-02",
"day": "Fri",
"delivered": 1500,
"failed": 10,
"pending": 50
}
]