Introduction
SMS delivery across Nigerian telecom networks via the MOAA TECH platform.
The MOAA TECH SMS API enables programmatic SMS delivery across all major Nigerian telecom networks: MTN, Airtel, Glo, and 9mobile. Messages are routed through one of two route types depending on your use case.
Transactional Route
For OTPs, transaction alerts, and time sensitive notifications. Highest delivery priority. Exempt from Do Not Disturb (DND) filtering. Sub second dispatch.
Promotional Route
For bulk campaigns and marketing messages. DND compliant. Supports scheduled delivery. Cost effective for high volume sends.
+234 prefix (e.g. +2348031234567). A single SMS unit covers up to 160 characters for plain text or 70 characters for unicode.
Authentication
All API requests require a valid Bearer token or API key in the Authorization header.
Include your token in every request using the Authorization header. Bearer tokens are obtained through the login flow and expire after 15 minutes. For server to server integration, use API keys instead. API keys are prefixed with mt_ and do not expire.
# Using a Bearer token curl https://api.moaatech.com/api/sms/transactional \ -H "Authorization: Bearer eyJhbGciOi..." # Using an API key curl https://api.moaatech.com/api/sms/transactional \ -H "Authorization: Bearer mt_your_api_key_here"
const headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' };
Send Transactional SMS
Send a high priority, DND exempt SMS for OTPs, alerts, and transaction notifications.
Delivers a single SMS via the transactional route. Messages sent through this endpoint bypass DND filters and are dispatched with highest priority. Ideal for OTP codes, payment confirmations, and time critical alerts.
| Field | Type | Description |
|---|---|---|
| to required | string | Recipient phone number in international format (e.g. +2348031234567) |
| sender required | string | Approved sender ID (e.g. MTECH). Max 11 characters. |
| message required | string | SMS body content. 160 chars per unit (plain) or 70 chars (unicode). |
| type optional | string | plain or unicode. Defaults to plain. |
curl -X POST https://api.moaatech.com/api/sms/transactional \ -H "Authorization: Bearer mt_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "to": "+2348031234567", "sender": "MTECH", "message": "Your verification code is 482910. Expires in 5 minutes." }'
const response = await fetch('https://api.moaatech.com/api/sms/transactional', { method: 'POST', headers: { 'Authorization': 'Bearer mt_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ to: '+2348031234567', sender: 'MTECH', message: 'Your verification code is 482910. Expires in 5 minutes.' }) }); const data = await response.json();
{
"success": true,
"messageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "queued",
"units": 1,
}
{
"success": false,
"error": "VALIDATION_ERROR",
"message": "\"to\" must be a valid phone number"
}
Send Enterprise SMS
Send SMS via the enterprise route with optional scheduling for future delivery.
Delivers a single SMS via the enterprise route. Supports all the same features as transactional with the addition of scheduled delivery. Suitable for business notifications, appointment reminders, and bulk communications.
| Field | Type | Description |
|---|---|---|
| to required | string | Recipient phone number in international format (e.g. +2348031234567) |
| sender required | string | Approved sender ID (e.g. MTECH). Max 11 characters. |
| message required | string | SMS body content. 160 chars per unit (plain) or 70 chars (unicode). |
| type optional | string | plain or unicode. Defaults to plain. |
| scheduledAt optional | string | ISO 8601 datetime for future delivery (e.g. 2026-04-10T09:00:00Z). Omit to send immediately. |
curl -X POST https://api.moaatech.com/api/sms/enterprise \ -H "Authorization: Bearer mt_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "to": "+2348031234567", "sender": "MTECH", "message": "Your appointment is confirmed for April 10 at 9:00 AM.", "scheduledAt": "2026-04-10T09:00:00Z" }'
const response = await fetch('https://api.moaatech.com/api/sms/enterprise', { method: 'POST', headers: { 'Authorization': 'Bearer mt_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ to: '+2348031234567', sender: 'MTECH', message: 'Your appointment is confirmed for April 10 at 9:00 AM.', scheduledAt: '2026-04-10T09:00:00Z' }) }); const data = await response.json();
{
"success": true,
"messageId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"status": "scheduled",
"units": 1,
}
{
"success": false,
"error": "UNAUTHORIZED",
"message": "Invalid or expired token"
}
Send OTP
Generate and deliver a one time password via SMS for identity verification.
Generates a random OTP, creates a session, and delivers the code via SMS. Each session tracks verification attempts and expires automatically. Built in fraud guards enforce per phone and per IP rate limits.
| Field | Type | Description |
|---|---|---|
| phone required | string | Recipient phone number (e.g. +2348031234567) |
| length optional | integer | OTP length: 4 or 6. Defaults to 6. |
| ttl optional | integer | Time to live in seconds. Defaults to 300 (5 minutes). |
curl -X POST https://api.moaatech.com/api/otp/send \ -H "Authorization: Bearer mt_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "phone": "+2348031234567", "length": 6, "ttl": 300 }'
const response = await fetch('https://api.moaatech.com/api/otp/send', { method: 'POST', headers: { 'Authorization': 'Bearer mt_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ phone: '+2348031234567', length: 6, ttl: 300 }) }); const data = await response.json();
{
"success": true,
"session_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"expires_in": 300,
"messageId": "d4e5f6a7-b8c9-0123-defa-234567890123"
}
429 RATE_LIMITED error.
Verify OTP
Validate a one time password against an active session.
Checks the submitted OTP against the session. Returns the verification result along with remaining attempts. Sessions are invalidated after successful verification or after all attempts are exhausted.
| Field | Type | Description |
|---|---|---|
| session_id required | string | Session ID returned from POST /api/otp/send |
| otp required | string | The OTP code entered by the user |
curl -X POST https://api.moaatech.com/api/otp/verify \ -H "Authorization: Bearer mt_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "session_id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "otp": "482910" }'
const response = await fetch('https://api.moaatech.com/api/otp/verify', { method: 'POST', headers: { 'Authorization': 'Bearer mt_your_api_key', 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: 'c3d4e5f6-a7b8-9012-cdef-123456789012', otp: '482910' }) }); const data = await response.json();
{
"verified": true,
"attemptsUsed": 1,
"attemptsRemaining": 2
}
{
"verified": false,
"attemptsUsed": 2,
"attemptsRemaining": 1
}
Message History
Retrieve paginated SMS delivery records with filtering.
Returns a paginated list of messages sent by your organization. Supports filtering by status, telco network, date range, sender ID, and channel.
| Field | Type | Description |
|---|---|---|
| page optional | integer | Page number. Defaults to 1. |
| limit optional | integer | Results per page. Defaults to 20, max 100. |
| status optional | string | Filter by status: queued, sent, delivered, failed |
| telco optional | string | Filter by network: MTN, Airtel, Glo, 9mobile |
| dateFrom optional | string | Start date (ISO 8601). E.g. 2026-04-01T00:00:00Z |
| dateTo optional | string | End date (ISO 8601). E.g. 2026-04-05T23:59:59Z |
| senderId optional | string | Filter by sender ID (e.g. MTECH) |
| channel optional | string | Filter by channel: sms |
curl "https://api.moaatech.com/api/messages?page=1&limit=20&status=delivered&telco=MTN" \
-H "Authorization: Bearer mt_your_api_key"
const params = new URLSearchParams({ page: '1', limit: '20', status: 'delivered', telco: 'MTN' }); const response = await fetch( `https://api.moaatech.com/api/messages?${params}`, { headers: { 'Authorization': 'Bearer mt_your_api_key' } } ); const data = await response.json();
{
"success": true,
"messages": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone": "+2348031234567",
"sender_id": "MTECH",
"channel": "sms",
"route_type": "transactional",
"status": "delivered",
"telco": "MTN",
"sms_units": 1,
"currency": "NGN",
"sent_at": "2026-04-05T10:30:00Z",
"delivered_at": "2026-04-05T10:30:02Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 142
}
}
Get Message
Retrieve details for a single message by its ID.
Returns the full details of a specific message including delivery status, cost, and timing information.
| Field | Type | Description |
|---|---|---|
| id required | string | The message UUID returned when the SMS was sent |
curl https://api.moaatech.com/api/messages/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer mt_your_api_key"
const messageId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; const response = await fetch( `https://api.moaatech.com/api/messages/${messageId}`, { headers: { 'Authorization': 'Bearer mt_your_api_key' } } ); const data = await response.json();
{
"success": true,
"message": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone": "+2348031234567",
"sender_id": "MTECH",
"channel": "sms",
"route_type": "transactional",
"status": "delivered",
"telco": "MTN",
"message_body": "Your verification code is 482910. Expires in 5 minutes.",
"message_length": 55,
"sms_units": 1,
"sent_at": "2026-04-05T10:30:00Z",
"delivered_at": "2026-04-05T10:30:02Z",
"latency_ms": 2100,
"created_at": "2026-04-05T10:30:00Z"
}
}
Sender IDs
List the sender IDs registered to your organization.
Returns all sender IDs associated with your organization, including their approval status and purpose. Only sender IDs with active status can be used to send messages.
curl https://api.moaatech.com/api/sender-ids \
-H "Authorization: Bearer mt_your_api_key"
const response = await fetch('https://api.moaatech.com/api/sender-ids', { headers: { 'Authorization': 'Bearer mt_your_api_key' } }); const data = await response.json();
{
"success": true,
"senderIds": [
{
"id": "e5f6a7b8-c9d0-1234-efab-567890123456",
"sender_id": "MTECH",
"status": "active",
"purpose": "transactional",
"approved_at": "2026-01-15T12:00:00Z"
},
{
"id": "f6a7b8c9-d0e1-2345-fabc-678901234567",
"sender_id": "MTECH-MKT",
"status": "active",
"purpose": "promotional",
"approved_at": "2026-01-15T12:00:00Z"
}
]
}
Webhooks
Receive real time delivery status updates via HTTP callbacks.
MOAA TECH sends outbound HTTP POST requests to your configured callback URL when message delivery events occur. Configure your webhook URL in Console → Settings → Webhooks.
MOAA TECH will POST to your callback URL with the following payload when a message status changes. Events include message.delivered and message.failed.
| Field | Type | Description |
|---|---|---|
| event | string | message.delivered or message.failed |
| message_id | string | UUID of the message |
| phone | string | Recipient phone number |
| sender_id | string | Sender ID used |
| status | string | delivered or failed |
| delivered_at | string | ISO 8601 timestamp (present for delivered events) |
{
"event": "message.delivered",
"message_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"phone": "+2348031234567",
"sender_id": "MTECH",
"status": "delivered",
"delivered_at": "2026-04-05T10:30:02Z"
}
Every webhook request includes an X-Moaa-Signature header containing an HMAC SHA256 signature of the raw request body, signed with your webhook secret. Always verify this signature to confirm the request originated from MOAA TECH.
const crypto = require('crypto'); function verifyWebhook(req, secret) { const signature = req.headers['x-moaa-signature']; if (!signature) return false; const expected = crypto .createHmac('sha256', secret) .update(req.rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // Express route handler app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { if (!verifyWebhook(req, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body); // Process the webhook event... res.status(200).send('OK'); });
crypto.timingSafeEqual) when verifying signatures to prevent timing attacks. Your webhook secret is available in Console → Settings → Webhooks.
Error Codes
Standard error responses returned by the API.
All error responses follow a consistent structure with success: false, an error code, and a human readable message.
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
Request body or query parameters failed validation. Check the message field for specifics. |
| 401 | UNAUTHORIZED |
Missing, invalid, or expired authentication token. |
| 402 | INSUFFICIENT_BALANCE |
Wallet balance is too low to cover the message cost. Top up via Console. |
| 403 | FORBIDDEN |
Authenticated but lacking permission for this action or resource. |
| 404 | NOT_FOUND |
The requested resource does not exist. |
| 429 | RATE_LIMITED |
Too many requests. Back off and retry after the period indicated in the Retry-After header. |
| 500 | INTERNAL_ERROR |
An unexpected server error occurred. Contact support if it persists. |
{
"success": false,
"error": "INSUFFICIENT_BALANCE",
"message": "Insufficient wallet balance for this request."
}
Rate Limits
Request throttling applied to all API endpoints.
All API endpoints are rate limited to ensure fair usage. Rate limit information is returned in response headers on every request.
429 RATE_LIMITED with a Retry-After header indicating how many seconds to wait before retrying. Need a higher limit? Contact support to request an increase for your account.
{
"success": false,
"error": "RATE_LIMITED",
"message": "Too many requests. Please retry after 45 seconds."
}
Sandbox Mode
Test your integration without dispatching real SMS messages.
Enable sandbox mode in Console → Settings → Sandbox to test API calls without sending real SMS messages. All endpoints behave normally, but no messages are actually dispatched. Message status is simulated as delivered and no wallet balance is deducted.
{
"success": true,
"messageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "sandbox",
"units": 1,
"sandbox": true
}