Mora SMS API
A powerful and easy-to-use SMS API for sending messages, managing sender names, and checking account balances.
https://mora-sa.com/api/v1/
🚀 Introduction
Welcome to the Mora SMS API documentation. Our RESTful API allows you to integrate SMS functionality into your applications with ease. Whether you need to send notifications, verification codes, or marketing messages, our API provides reliable and fast delivery.
Key Features
- Send SMS messages to single or multiple recipients
- Schedule messages for future delivery
- Check account balance in real-time
- Manage sender names
- Comprehensive error handling
- JSON and text response formats
🔐 Authentication
All API requests require authentication using your unique api_key and username. These parameters must be included in every request.
https://mora-sa.com/api/v1/sendsms?api_key=your_api_key&username=your_username
📱 Sending SMS
Send SMS messages to one or multiple recipients using the /sendsms endpoint.
Endpoint
https://mora-sa.com/api/v1/sendsms
Parameters
| Parameter | Type | Description |
|---|---|---|
message Required |
string | The SMS message content to be sent |
sender Required |
string | The sender name/ID to be used for sending |
numbers Required |
string | Comma-separated list of phone numbers (e.g., "966501234567,966507654321") |
datetime Optional |
string | Schedule message for future delivery (format: YYYY-MM-DD HH:MM:SS) |
return Optional |
string | Response format: "json" (default) or "text" |
Code Examples
curl -X POST "https://mora-sa.com/api/v1/sendsms" \
-d "api_key=your_api_key" \
-d "username=your_username" \
-d "message=Hello from Mora SMS API!" \
-d "sender=YourApp" \
-d "numbers=966501234567,966507654321" \
-d "return=json"
<?php
$url = 'https://mora-sa.com/api/v1/sendsms';
$data = array(
'api_key' => 'your_api_key',
'username' => 'your_username',
'message' => 'Hello from Mora SMS API!',
'sender' => 'YourApp',
'numbers' => '966501234567,966507654321',
'return' => 'json'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
?>
import requests
url = 'https://mora-sa.com/api/v1/sendsms'
data = {
'api_key': 'your_api_key',
'username': 'your_username',
'message': 'Hello from Mora SMS API!',
'sender': 'YourApp',
'numbers': '966501234567,966507654321',
'return': 'json'
}
response = requests.post(url, data=data)
print(response.json())
const data = new URLSearchParams({
api_key: 'your_api_key',
username: 'your_username',
message: 'Hello from Mora SMS API!',
sender: 'YourApp',
numbers: '966501234567,966507654321',
return: 'json'
});
fetch('https://mora-sa.com/api/v1/sendsms', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
💰 Balance Inquiry
Check your account balance using the /balance endpoint.
Endpoint
https://mora-sa.com/api/v1/balance
Code Examples
curl "https://mora-sa.com/api/v1/balance?api_key=your_api_key&username=your_username"
<?php
$url = 'https://mora-sa.com/api/v1/balance';
$params = array(
'api_key' => 'your_api_key',
'username' => 'your_username'
);
$url .= '?' . http_build_query($params);
$response = file_get_contents($url);
echo $response;
?>
import requests
url = 'https://mora-sa.com/api/v1/balance'
params = {
'api_key': 'your_api_key',
'username': 'your_username'
}
response = requests.get(url, params=params)
print(response.json())
const params = new URLSearchParams({
api_key: 'your_api_key',
username: 'your_username'
});
fetch(`https://mora-sa.com/api/v1/balance?${params}`)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
👤 Sender Names
Retrieve available sender names for your account using the /sender_names endpoint.
Endpoint
https://mora-sa.com/api/v1/sender_names
Code Examples
curl "https://mora-sa.com/api/v1/sender_names?api_key=your_api_key&username=your_username"
<?php
$url = 'https://mora-sa.com/api/v1/sender_names';
$params = array(
'api_key' => 'your_api_key',
'username' => 'your_username'
);
$url .= '?' . http_build_query($params);
$response = file_get_contents($url);
echo $response;
?>
import requests
url = 'https://mora-sa.com/api/v1/sender_names'
params = {
'api_key': 'your_api_key',
'username': 'your_username'
}
response = requests.get(url, params=params)
print(response.json())
const params = new URLSearchParams({
api_key: 'your_api_key',
username: 'your_username'
});
fetch(`https://mora-sa.com/api/v1/sender_names?${params}`)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
⚠️ Error Codes
The API returns specific error codes to help you troubleshoot issues. Here's a complete list of possible response codes:
| Code | Description |
|---|---|
| 100 | ✅ Numbers received successfully |
| 105 | ❌ Insufficient balance |
| 106 | ❌ Sender name not available |
| 107 | ❌ Sender name is blocked |
| 108 | ❌ No valid numbers for sending |
| 112 | ❌ Message contains prohibited words |
| 114 | ❌ Account is suspended |
| 115 | ❌ Mobile number not activated |
| 116 | ❌ Email not activated |
| 117 | ❌ Empty message cannot be sent |
| 118 | ❌ Sender name is empty |
| 119 | ❌ No recipient number provided |
🔗 Webhook Integration
The platform can push the status of every message you send to a URL of your own (your "webhook"). Instead of polling the API, you receive an HTTP POST with the message and its per-number statuses.
1. Enable your webhook
- Log in to the portal and open API Keys (
/api_keys). - In the Webhook Settings card, enter your endpoint URL.
- Press Activate. (To stop receiving calls, press Disable on the same card).
URL Requirements
| Rule | Detail |
|---|---|
| Scheme | http or https only (use https) |
| Host | Must be public - private, loopback, and link-local addresses (127.0.0.1, 10.x, 192.168.x, 169.254.x, ...) are rejected |
| Length | Max 2048 characters |
| Redirects | Not followed - give the final URL, a 301/302 counts as a failure |
2. What we send
A single POST per message, with Content-Type: application/json.
{
"msg_id": 8842137,
"sending_time": "2026-08-24 10:15:32",
"numbers_count": 3,
"status": "delivered",
"numbers": [
{ "number": "966500000001", "status": "delivered" },
{ "number": "966500000002", "status": "sending" },
{ "number": "966500000003", "status": "failed", "response": "رقم الجوال خطأ" }
]
}
Payload Fields
| Field | Type | Description |
|---|---|---|
msg_id |
integer | The message id, same id returned by the send API. Use it as the idempotency key. |
sending_time |
string | YYYY-MM-DD HH:MM:SS, last update time of the message (server time). |
numbers_count |
integer | How many entries are in numbers. |
status |
string | Overall status of the message. |
numbers |
array | One entry per recipient. |
numbers[].number |
string | The recipient number. |
numbers[].status |
string | Status of that recipient. |
numbers[].response |
string | Only present when the status is failed; the reason (gateway or validation error text, may be Arabic). |
Status Values
| Value | Meaning |
|---|---|
pending |
Accepted, not handed to a gateway yet (may be awaiting approval or queued). |
sending |
Handed to the gateway, in progress. |
delivered |
Successfully sent / accepted by the gateway. |
failed |
Rejected or not sent - see response for the reason. |
delivered means the gateway accepted the message. Operator delivery receipts (DLR) are not included in the webhook payload; check the reports screen or the API if you need the final DLR.
3. How your endpoint must behave
- Answer 2xx: Any other status code (4xx, 5xx), a redirect, a TLS error, or a timeout is treated as a delivery failure.
- Answer fast: Timeouts are 5 s to connect and 10 s in total. Queue the payload and process it asynchronously - don't do slow work in the handler.
- Be idempotent: A call may repeat (for example if we time out after your server already processed it). Deduplicate on
msg_id. - Ordering: Don't rely on ordering guarantees beyond
msg_id. Messages are pushed in ascendingmsg_idorder, one at a time.
Securing your endpoint
No signature or authorization header is sent. Protect the endpoint by:
- Using a long unguessable path or query token, e.g.,
https://example.com/hooks/sms/9f2c1b7e5a..., and rejecting anything else. - Serving it over HTTPS.
- Optionally allowlisting the platform's outbound IP address.
4. Delivery, retries and auto-disable
| Behaviour | Value |
|---|---|
| Schedule | Every minute |
| Messages per run | Up to 500 per account; the rest go out on the next runs |
| Retry on failure | The same message is retried every minute, position is not advanced |
| Auto-disable | After 10 consecutive failed runs (~10 minutes of downtime) |
| Maximum replay window | 2 days - anything older is skipped permanently |
Each message is pushed once, shortly after it is created - the payload shows the statuses as they are at that moment, not a final report.
5. Example receivers
<?php
// https://example.com/hooks/sms/<secret>
if (($_GET['token'] ?? '') !== 'YOUR_SECRET') {
http_response_code(404);
exit;
}
$payload = json_decode(file_get_contents('php://input'), true);
http_response_code(200); // answer first, work later
fastcgi_finish_request();
foreach ($payload['numbers'] as $n) {
// store/update by $payload['msg_id'] + $n['number']
// $n['status'], $n['response'] ?? null
}
?>
// routes/api.php
Route::post('hooks/sms/{token}', function (Request $request, $token) {
abort_unless(hash_equals(config('services.sms.hook_token'), $token), 404);
ProcessSmsWebhook::dispatch($request->all()); // queue it
return response()->json(['ok' => true]);
});
app.post('/hooks/sms/:token', express.json(), (req, res) => {
if (req.params.token !== process.env.SMS_HOOK_TOKEN) return res.sendStatus(404);
res.sendStatus(200); // ack immediately
queue.add('sms-status', req.body);
});
6. Webhook log
Every delivery attempt is recorded and shown in the portal under API Keys → Webhook Log (/webhook/logs). Each row holds the time, the message id (linking to the message), whether the call succeeded, the HTTP status code your endpoint returned, how long it took, and the error text when it failed. You can filter by outcome (delivered / failed) or by a specific message id.
Attempts are kept for 7 days and pruned automatically.
Use it to answer the usual questions without contacting support: did the call reach my server? (a row exists), what did my server answer? (HTTP Code), why was my webhook disabled? (a run of failed rows), and is my endpoint too slow? (Duration approaching 10 000 ms).
7. Troubleshooting
| Symptom | Likely cause |
|---|---|
| URL rejected when saving | Non-public host, wrong scheme, or DNS does not resolve |
| No calls arriving | Webhook disabled (غير مفعل) or no new messages since it was activated |
| Webhook keeps disabling itself | Endpoint returns non-2xx, redirects, or takes longer than 10 s |
| Some numbers missing response | Normal - response is only sent for failed entries |
| No row in the Webhook Log at all | The message predates activation, or the webhook is disabled |
| Statuses look "too early" | Expected - the push happens within a minute of sending; use the reports/API for final DLR |