Overview
What you can build
- Bulk and single SMS — send one message or thousands in a single request, immediately or scheduled for a future date.
- Your own sender name — send under your approved brand name instead of a shared system sender.
- OTP verification — generate and verify one-time passcodes for login, signup or checkout, by SMS and optionally email.
- Delivery tracking — see what you have sent, and receive status updates on your own server.
Quickstart
Once you have an API key, sending an SMS is one request:
curl -X POST https://portal.eroyal.africa/api/v1/messages \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sender_name": "MYBRAND",
"is_scheduled": false,
"messages": [
{ "receiver": "0712345678", "content": "Hello Asha, your order is ready" }
]
}'
Every response, success or failure, comes back in the same envelope, so you can write one response handler for the whole API.
Authentication
Every endpoint except POST /otps/verify requires an API key, sent as a
standard Authorization header.
API keys
Generate a key from your Eroyal Africa account, not from the API: sign in to
portal.eroyal.africa, open
API and press New API Key. Give it a name so you can tell your keys apart —
one per environment or integration. The raw key is shown once, and every key begins
txf_:
txf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Using a key
Send it in the Authorization header, as a bearer token, on any protected
endpoint:
curl https://portal.eroyal.africa/api/v1/messages \ -H "Authorization: Bearer txf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Keys do not expire and are not tied to a session, so there is no logout for them.
Authentication errors
{
"success": false,
"status_code": 401,
"status_text": "Unauthorized",
"message": "invalid api key",
"error": "invalid_token"
}
| Status | error | Meaning |
|---|---|---|
| 401 | missing_token | No Authorization header was sent. |
| 401 | invalid_token | The key does not match any account, or it has been deleted. |
| 503 | not_configured | The server is not set up to issue or check keys yet. |
Base URL
Every endpoint in this reference is relative to a single base URL:
https://portal.eroyal.africa/api/v1
So sending an SMS means a request to https://portal.eroyal.africa/api/v1/messages. Throughout this
reference, paths are written relative to that base, e.g. POST /messages.
Versioning
/api/v1 is the current and only stable version. Breaking changes will arrive
under a new prefix rather than changing this one in place.
Request & Response Format
All request bodies are JSON. Send Content-Type: application/json on every
request that carries a body, and authenticate as described above unless the endpoint is
marked public.
Response envelope
{
"success": true,
"status_code": 200,
"status_text": "OK",
"message": "messages have been fetched",
"data": { }
}
{
"success": false,
"status_code": 400,
"status_text": "Bad Request",
"message": "no receiver has been provided",
"error": "validation_error"
}
success tells you which shape you got without inspecting the status code. On
success, data holds the payload and error is omitted. On failure,
error is a short machine-readable code you can branch on, and
message is a human-readable description safe to log.
Pagination
| Param | Default | Description |
|---|---|---|
page | 1 | Page number. |
limit | 10 | Records per page (max 1000). |
sort_dir | desc | asc or desc, by creation time. |
status | — | Filter by exact status. |
{
"success": true,
"status_code": 200,
"status_text": "OK",
"message": "messages have been fetched",
"data": {
"data": [ { "id": "…", "status": "sending" } ],
"total": 42,
"page": 1,
"limit": 10,
"total_pages": 5,
"has_next_page": true,
"has_previous_page": false,
"next_page": 2,
"previous_page": null
}
}
HTTP status codes
| Status | Status text | Meaning |
|---|---|---|
| 200 | OK | Request succeeded. |
| 201 | Created | Messages were queued. |
| 400 | Bad Request | Missing or invalid fields — see error and message. |
| 401 | Unauthorized | Missing or invalid API key. |
| 404 | Not Found | The route or the record does not exist. |
| 405 | Method Not Allowed | The method is not supported on this path. |
| 500 | Internal Server Error | Unexpected server-side failure. |
SMS API
Send single or bulk SMS and list what you have sent. One SMS unit is 160 characters; longer
content is billed as ceil(length / 160) parts.
POST /messages
Creates and queues one message per recipient. If you pass a sender name it must be one approved for your account. Your balance is checked and deducted as the messages are queued.
| Field | Type | Required | Description |
|---|---|---|---|
messages | array | required | One or more { receiver, content } objects. Non-empty. |
messages[].receiver | string | required | Recipient phone number. Normalised to 255… international format. |
messages[].content | string | required | Message text. 160 characters is one billed unit. |
sender_name | string | optional | An approved sender name on your account. Omit to send under the system sender. |
is_scheduled | boolean | optional | Defaults to false. When true, scheduled_date is required. |
scheduled_date | string (ISO 8601) | optional | When it falls due the message moves from scheduled to sending. |
Bulk
{
"sender_name": "MYBRAND",
"is_scheduled": false,
"messages": [
{ "receiver": "0711111111", "content": "Hello Asha, your order is ready" },
{ "receiver": "0722222222", "content": "Hello Juma, your order is ready" }
]
}
Scheduled
{
"sender_name": "MYBRAND",
"is_scheduled": true,
"scheduled_date": "2026-12-20T08:00:00Z",
"messages": [
{ "receiver": "0712345678", "content": "Reminder: your appointment is tomorrow" }
]
}
Response
The messages are queued, not returned inline. You get back the id of the batch, which you can look up afterwards.
{
"success": true,
"status_code": 201,
"status_text": "Created",
"message": "messages have been created",
"data": {
"id": "6b2f6e2a4b0e4e9a9a2e1a2b3c4d5e6f",
"recipients": 2,
"count": 2,
"status": "sending"
}
}
sending, or
scheduled until it is due. From there each message moves through
processing → sent → delivered |
undelivered | failed.Errors
| Status | error | Meaning |
|---|---|---|
| 400 | validation_error | No messages, a missing receiver or empty content, or scheduled_date missing. |
| 400 | creation_error | The sender name is not approved for your account, or your balance is not enough. |
| 401 | invalid_token | Missing or invalid API key. |
GET /messages
Paginated list of what this account has sent, newest first. Accepts page,
limit, status and sort_dir.
curl "https://portal.eroyal.africa/api/v1/messages?page=1&limit=20&sort_dir=desc" \ -H "Authorization: Bearer $API_KEY"
GET /messages/{id}
One batch, by the id returned when you sent it.
{
"success": true,
"status_code": 200,
"status_text": "OK",
"message": "message has been fetched",
"data": {
"id": "6b2f6e2a4b0e4e9a9a2e1a2b3c4d5e6f",
"recipients": 2,
"count": 2,
"sender_name": "MYBRAND",
"content": "Hello Asha, your order is ready",
"is_scheduled": false,
"scheduled_date": null,
"status": "sending",
"created_at": "2026-09-29T08:12:41+00:00"
}
}
| Status | error | Meaning |
|---|---|---|
| 404 | not_found | No batch exists with that id on this account. |
OTP API
Send one-time passcodes for login, signup or transaction confirmation, and verify them server-side. Codes are single-use and expire after 30 minutes.
POST /otps
Sends a 6-digit code by SMS, and by email as well if you give an address.
| Field | Type | Required | Description |
|---|---|---|---|
phone_number | string | required | Recipient number. Normalised to 255… format. |
email_address | string | optional | When set, the code is emailed as well. |
sender_name | string | optional | An approved sender name on your account. |
brand_name | string | optional | The brand written into the message. Defaults to your account's. |
curl -X POST https://portal.eroyal.africa/api/v1/otps \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "0712345678",
"email_address": "customer@example.com",
"brand_name": "My Brand"
}'
POST /otps/verify Public
Consumes a code. Each code can be verified once. This endpoint needs no API key, so it can be called straight from a signup or login screen.
curl -X POST https://portal.eroyal.africa/api/v1/otps/verify \
-H "Content-Type: application/json" \
-d '{ "code": 484895, "phone_number": "0712345678" }'
| Status | error | Meaning |
|---|---|---|
| 400 | validation_error | code was missing. |
| 400 | verification_error | No matching, unexpired code for that number or address. |
Webhooks
Rather than polling, you can have delivery updates pushed to your own server. Tell us the URL and we POST to it when a message you sent lands or fails.
{
"entity": "messages",
"id": "6b2f6e2a-4b0e-4e9a-9a2e-1a2b3c4d5e6f",
"status": "delivered"
}
Respond quickly with any 2xx status to acknowledge receipt. Treat a callback as a prompt to re-check rather than as proof on its own, and keep the endpoint fast and reachable.
Support
Stuck on an integration, a sender name or your balance? Reach the Eroyal Africa team:
Office
Kigamboni, Dar es Salaam, Tanzania
+255 765 492 700
Before you write
- Check the
errorcode in the response against the tables above — most integration problems are named there directly. - Have your account email to hand, and the batch id if it is about a send.