Introduction
The AdaSMS.ma API sends single or bulk SMS to Moroccan numbers, lets you track every message, share documents through secure links (AdaLink) and check your balance. Your systems can also be notified in real time by webhook.
Base URL
https://api.adasms.ma/v1Every request uses HTTPS. Request and response bodies are JSON (except AdaLink file uploads, which use multipart/form-data).
Response format
Every response contains success (true or false). On error, message explains the problem and, for errors your code should handle, code gives a stable value (see Error codes).
message) are written in French. In your code, rely on the HTTP status and the code field, never on the message text.Authentication
Every request carries your API key in the X-API-Key header. Create your keys in your customer area, API Keys menu: one key per application, which you can disable at any time.
X-API-Key: adasms_YOUR_KEYA key is made of adasms_ followed by 48 hexadecimal characters.
Authentication errors
| HTTP status | Code | Cause |
|---|---|---|
| 401 | — | Missing X-API-Key header |
| 401 | — | Unknown, disabled or malformed key |
| 403 | — | Account suspended |
| 403 | SENDER_EXPIRED | Key linked to an expired or inactive Sender ID |
| 429 | RATE_LIMITED | More than 20 malformed keys from the same IP address within 10 minutes |
Phone numbers
AdaSMS.ma only sends to Moroccan numbers. Three formats are accepted everywhere (single send, bulk send, AdaLink); spaces, dashes, dots and brackets are ignored. Numbers are stored and returned in the +212XXXXXXXXX format.
| Format | Example | Stored as |
|---|---|---|
+212XXXXXXXXX | +212600000001 | +212600000001 |
212XXXXXXXXX | 212600000001 | +212600000001 |
0XXXXXXXXX | 06 00 00 00 01 | +212600000001 |
An unreadable number returns a 400 error. A foreign number (for example +33…) returns a 400 error with the code NON_MOROCCAN_NUMBER, and nothing is debited; in a bulk send, the whole request is then rejected.
Segments and encoding
An SMS is billed per segment. The number of segments depends on the text length and its encoding, which is detected automatically:
| Encoding | 1 segment | Several segments |
|---|---|---|
| GSM-7 (standard alphabet) | 160 characters | 153 characters per segment |
| Unicode / UCS-2 (Arabic, emojis, some accents) | 70 characters | 67 characters per segment |
A text is limited to 10 segments (1,530 GSM-7 characters or 670 in Unicode); beyond that, the API returns 400 TEXT_TOO_LONG. In GSM-7, the characters ^ { } \ [ ] ~ | € count twice.
ê â î ô û ë ï ç. With gsm_replace: true, these characters are replaced with their GSM-7 equivalent before sending.Rate limits
POST /messages, POST /bulk and GET /account are limited per 60-second window:
| Scope | Limit per 60 s window |
|---|---|
| Per API key | 600 requests |
| Per IP address | 1,200 requests |
Beyond that, the API returns 429 with the code RATE_LIMITED and the Retry-After header (delay in seconds). To send many messages, prefer POST /bulk (up to 1,000 recipients per request). Need a higher limit? Write to support@adasms.ma.
API reference
Send an SMS
/v1/messagesSends an SMS to a Moroccan number. The cost is debited when the message is sent (your plan's unit price × number of segments). If the operator rejects the message, the amount is credited back to your balance.
Subject to rate limiting. See ›
Request body
JSON body.
| Field | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Moroccan number: +212XXXXXXXXX, 212XXXXXXXXX or 0XXXXXXXXX (spaces, dashes, dots and brackets ignored). Converted to the +212XXXXXXXXX format. |
text | string | Yes | SMS text, at most 10 segments (1,530 GSM-7 characters or 670 in Unicode). See “Segments and encoding”. |
from | string | — | Your approved Sender ID (sender name). Required as soon as you have at least one approved Sender ID. Without an approved Sender ID, leave it empty: the shared sender is used for a limited number of trial messages. If your API key is linked to a Sender ID, that one is always used. |
type | string | — | Free label (e.g. otp, alert, promo), kept on the message and in webhooks. Default: transactional |
reference | string | — | Your own reference (printable ASCII, 64 characters maximum), to find your messages with GET /messages?reference=…. |
gsm_replace | boolean | — | Replaces common Unicode characters with their GSM-7 equivalent before sending (ê→e, ç→c, «→", —→-, …→...). The stored text is the replaced text. Default: false |
Request
curl -X POST "https://api.adasms.ma/v1/messages" \
-H "X-API-Key: adasms_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+212600000001",
"text": "Your verification code: 847291",
"from": "MyService",
"type": "otp",
"reference": "ORDER-2026-001"
}'Response 202
Message accepted.
{
"success": true,
"messageId": "3f6c2a91-5b7e-4d0a-9c1e-2b8f4e6d7a10",
"status": "sent",
"providerMessageId": "9a8b7c6d-0000-4e1f-8a2b-3c4d5e6f7a8b",
"cost": "0.4900",
"channel": "sms",
"downgraded": false,
"segments": 1,
"encoding": "GSM7",
"reference": "CMD-2026-001"
}Response fields
| Field | Type | Description |
|---|---|---|
messageId | string | |
status | string | sent = handed to the operator; queued = waiting to be handed over. Values: queued, sent |
providerMessageId | string | Identifier assigned by the operator. |
cost | string | Amount debited in MAD excl. VAT (price per segment × segments), 4 decimals. |
provider | string | Deprecated Technical field kept for compatibility; its value may change without notice. Do not use it. |
channel | string | Values: sms |
downgraded | boolean | true if the message was sent by SMS although a rule on your account planned another channel. |
segments | integer | |
encoding | string | Values: GSM7, UCS2 |
reference | string | null |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 400 | — | to or text missing |
| 400 | — | Unreadable number (accepted formats, see “Phone numbers”) |
| 400 | NON_MOROCCAN_NUMBER | Number outside Morocco |
| 400 | — | Invalid reference (printable ASCII, 64 characters maximum) |
| 400 | TEXT_TOO_LONG | Text longer than 10 segments |
| 400 | DEFAULT_SENDER_FORBIDDEN | from missing although you have an approved Sender ID |
| 400 | DEFAULT_SENDER_QUOTA_EXCEEDED | Trial messages with the shared sender used up |
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
| 402 | INSUFFICIENT_CREDIT | Insufficient balance |
| 403 | EMAIL_NOT_VERIFIED | Account email address not confirmed |
| 429 | RATE_LIMITED | Rate limit reached |
| 502 | — | Sending failed |
Search by reference
/v1/messagesReturns your messages whose reference field exactly matches the given value: at most 100, newest first.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
reference | string | Yes | Reference given when sending. |
Request
curl "https://api.adasms.ma/v1/messages?reference=CMD-2026-001" \
-H "X-API-Key: adasms_YOUR_KEY"Response 200
Matching messages (possibly empty list).
{
"success": true,
"messages": [
{
"id": "3f6c2a91-5b7e-4d0a-9c1e-2b8f4e6d7a10",
"to": "+212600000001",
"from": "MyService",
"type": "otp",
"channel": "sms",
"status": "sent",
"text": "Your verification code: 847291",
"cost": "0.49",
"billingOutcome": "charged",
"segments": 1,
"encoding": "GSM7",
"reference": "CMD-2026-001",
"providerMessageId": "9a8b7c6d-0000-4e1f-8a2b-3c4d5e6f7a8b",
"errorMessage": null,
"createdAt": "2026-11-10T09:00:00.000Z",
"updatedAt": "2026-11-10T09:00:01.200Z"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
messages | array<object> | |
messages[].id | string | |
messages[].to | string | Number in +212XXXXXXXXX format. |
messages[].from | string | null | |
messages[].type | string | |
messages[].channel | string | Values: sms |
messages[].status | string | queued = waiting; sent = handed to the operator; delivered = delivered to the phone (operator-dependent, not available yet); failed = failed. Values: queued, sent, delivered, failed |
messages[].text | string | |
messages[].cost | string | Amount debited in MAD excl. VAT ("0" if sending failed). |
messages[].provider | string | null | Deprecated Technical field kept for compatibility; do not use it. |
messages[].billingOutcome | string | charged = billed; refunded = credited back after an incident; not_charged = rejected before sending, not billed. Values: charged, refunded, not_charged |
messages[].segments | integer | |
messages[].encoding | string | Values: GSM7, UCS2 |
messages[].reference | string | null | |
messages[].providerMessageId | string | null | |
messages[].errorMessage | string | null | Failure reason, free text. |
messages[].createdAt | string | |
messages[].updatedAt | string |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 400 | — | Missing reference parameter |
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
Retrieve a message
/v1/messages/{messageId}Returns one of your messages.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
messageId | string | Yes | Identifier returned when sending (messageId). |
Request
curl "https://api.adasms.ma/v1/messages/3f6c2a91-5b7e-4d0a-9c1e-2b8f4e6d7a10" \
-H "X-API-Key: adasms_YOUR_KEY"Response 200
The message.
{
"success": true,
"message": {
"id": "3f6c2a91-5b7e-4d0a-9c1e-2b8f4e6d7a10",
"to": "+212600000001",
"from": "MyService",
"type": "otp",
"channel": "sms",
"status": "sent",
"text": "Your verification code: 847291",
"cost": "0.49",
"billingOutcome": "charged",
"segments": 1,
"encoding": "GSM7",
"reference": "CMD-2026-001",
"providerMessageId": "9a8b7c6d-0000-4e1f-8a2b-3c4d5e6f7a8b",
"errorMessage": null,
"createdAt": "2026-11-10T09:00:00.000Z",
"updatedAt": "2026-11-10T09:00:01.200Z"
}
}Response fields
| Field | Type | Description |
|---|---|---|
message | object | |
message.id | string | |
message.to | string | Number in +212XXXXXXXXX format. |
message.from | string | null | |
message.type | string | |
message.channel | string | Values: sms |
message.status | string | queued = waiting; sent = handed to the operator; delivered = delivered to the phone (operator-dependent, not available yet); failed = failed. Values: queued, sent, delivered, failed |
message.text | string | |
message.cost | string | Amount debited in MAD excl. VAT ("0" if sending failed). |
message.provider | string | null | Deprecated Technical field kept for compatibility; do not use it. |
message.billingOutcome | string | charged = billed; refunded = credited back after an incident; not_charged = rejected before sending, not billed. Values: charged, refunded, not_charged |
message.segments | integer | |
message.encoding | string | Values: GSM7, UCS2 |
message.reference | string | null | |
message.providerMessageId | string | null | |
message.errorMessage | string | null | Failure reason, free text. |
message.createdAt | string | |
message.updatedAt | string |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
| 404 | — | Message not found |
Send bulk SMS
/v1/bulkSends up to 1,000 SMS in one request, with a common text or one text per recipient. Numbers accept the same formats as single sends. Duplicates (once converted to +212 format) are sent only once. The total cost is checked and debited before sending; each message rejected by the operator is credited back. If a single number is invalid or outside Morocco, the whole request is rejected and nothing is debited.
Subject to rate limiting. See ›
Request body
JSON body.
| Field | Type | Required | Description |
|---|---|---|---|
recipients | array<string | object> | Yes | Numbers, or {to, text, reference} objects. 1,000 at most. |
recipients[].to | string | Yes | Moroccan number: +212XXXXXXXXX, 212XXXXXXXXX or 0XXXXXXXXX (spaces, dashes, dots and brackets ignored). Converted to the +212XXXXXXXXX format. |
recipients[].text | string | — | Text for this recipient (otherwise the common text). |
recipients[].reference | string | — | Your own reference (printable ASCII, 64 characters maximum), to find your messages with GET /messages?reference=…. |
text | string | — | Common text, required if a recipient has no text of its own. |
from | string | — | Your approved Sender ID (same rules as POST /messages). |
type | string | — | Free label. Default: bulk |
gsm_replace | boolean | — | GSM-7 replacement applied to every text (see POST /messages). Default: false |
Request
Common text
curl -X POST "https://api.adasms.ma/v1/bulk" \
-H "X-API-Key: adasms_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
"+212600000001",
"0600000002",
"212600000003"
],
"text": "Your order #4521 has shipped.",
"from": "MyShop"
}'One text per recipient
curl -X POST "https://api.adasms.ma/v1/bulk" \
-H "X-API-Key: adasms_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "MyShop",
"recipients": [
{
"to": "+212600000001",
"text": "Hello Karim, your balance is 250 MAD.",
"reference": "CUS-001"
},
{
"to": "0600000002",
"text": "Hello Salma, your balance is 80 MAD.",
"reference": "CUS-002"
}
]
}'Response 202
Batch processed. Each message's details are available with GET /bulk/{batchId}.
{
"success": true,
"batchId": "7c1d9e20-4a3b-4f6e-8d2c-1a0b9c8d7e6f",
"total": 3,
"status": "completed",
"estimatedCost": "1.4700"
}Response fields
| Field | Type | Description |
|---|---|---|
batchId | string | |
total | integer | Number of recipients once duplicates are removed. |
status | string | failed if every message failed. Values: completed, failed |
estimatedCost | string | Amount debited before sending, in MAD excl. VAT; failed messages are credited back. |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 400 | — | recipients missing or empty |
| 400 | — | More than 1,000 recipients |
| 400 | — | Unreadable number or missing text (details per recipient in errors) |
| 400 | NON_MOROCCAN_NUMBER | Number outside Morocco (details per recipient in errors) |
| 400 | — | Invalid reference (printable ASCII, 64 characters maximum) |
| 400 | TEXT_TOO_LONG | Text longer than 10 segments |
| 400 | DEFAULT_SENDER_FORBIDDEN | from missing although you have an approved Sender ID |
| 400 | DEFAULT_SENDER_QUOTA_EXCEEDED | Trial messages with the shared sender used up |
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
| 402 | INSUFFICIENT_CREDIT | Insufficient balance |
| 403 | EMAIL_NOT_VERIFIED | Account email address not confirmed |
| 429 | RATE_LIMITED | Rate limit reached |
| 502 | — | Sending failed |
Retrieve a batch
/v1/bulk/{batchId}Returns one of your batches and the status of each of its messages.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
batchId | string | Yes | Identifier returned when sending (batchId). |
Request
curl "https://api.adasms.ma/v1/bulk/7c1d9e20-4a3b-4f6e-8d2c-1a0b9c8d7e6f" \
-H "X-API-Key: adasms_YOUR_KEY"Response 200
The batch, its messages and their count.
{
"success": true,
"batch": {
"id": "7c1d9e20-4a3b-4f6e-8d2c-1a0b9c8d7e6f",
"clientId": "0b5e8f2a-1c3d-4e6f-9a7b-8c9d0e1f2a3b",
"type": "bulk",
"channel": "sms",
"status": "completed",
"totalMessages": 3,
"sentCount": 3,
"failedCount": 0,
"estimatedCost": "1.47",
"actualCost": "1.47",
"createdAt": "2026-11-10T09:00:00.000Z",
"updatedAt": "2026-11-10T09:00:01.200Z"
},
"messages": [
{
"id": "5e2a7b10-9c4d-4f1e-8a3b-6d7c8e9f0a1b",
"to": "+212600000001",
"status": "sent",
"providerMessageId": "9a8b7c6d-0000-4e1f-8a2b-3c4d5e6f7a8c",
"cost": "0.49",
"billingOutcome": "charged",
"errorMessage": null,
"createdAt": "2026-11-10T09:00:00.100Z"
}
],
"summary": {
"total": 3,
"sent": 3,
"failed": 0,
"pending": 0
}
}Response fields
| Field | Type | Description |
|---|---|---|
batch | object | |
batch.id | string | |
batch.clientId | string | |
batch.type | string | |
batch.channel | string | Values: sms |
batch.status | string | Values: processing, completed, failed |
batch.totalMessages | integer | |
batch.sentCount | integer | |
batch.failedCount | integer | |
batch.estimatedCost | string | |
batch.actualCost | string | |
batch.createdAt | string | |
batch.updatedAt | string | |
messages | array<object> | |
messages[].id | string | |
messages[].to | string | |
messages[].status | string | Values: queued, sent, delivered, failed |
messages[].providerMessageId | string | null | |
messages[].cost | string | |
messages[].billingOutcome | string | Values: charged, refunded, not_charged |
messages[].errorMessage | string | null | |
messages[].createdAt | string | |
summary | object | |
summary.total | integer | |
summary.sent | integer | |
summary.failed | integer | |
summary.pending | integer | Messages still waiting (queued). |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
| 404 | — | Batch not found |
Create and send an AdaLink
/v1/adalinkHosts a document in Morocco, creates a secure link valid for 1 to 30 days and sends it by SMS to the recipient. The link price (based on its duration) is debited on creation; the SMS carrying the link is charged on top, at your plan's price.
Request body
multipart/form-data body.
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | Document to share. PDF, JPG, PNG, WEBP, TXT or CSV, 10 MB at most. |
to | string | Yes | Moroccan number: +212XXXXXXXXX, 212XXXXXXXXX or 0XXXXXXXXX (spaces, dashes, dots and brackets ignored). Converted to the +212XXXXXXXXX format. |
durationDays | integer | Yes | How long the link stays valid, in days. Beyond 30 days, contact support for a quote. |
Request
curl -X POST "https://api.adasms.ma/v1/adalink" \
-H "X-API-Key: adasms_YOUR_KEY" \
-F "file=@invoice_2026_11.pdf" \
-F "to=+212600000001" \
-F "durationDays=14"Response 201
Link created; the SMS is sent right away.
{
"success": true,
"adaLink": {
"id": "2d4f6a8c-0e1b-4c3d-8e5f-7a9b0c1d2e3f",
"token": "Xk9mT2pQrL4vN8wZ",
"url": "https://doc.adasms.ma/l/Xk9mT2pQrL4vN8wZ",
"expiresAt": "2026-11-24T09:00:00.000Z",
"recipientPhone": "+212600000001",
"channel": "sms",
"durationDays": 14,
"costMad": "14.7000",
"originalName": "invoice_2026_11.pdf",
"fileSizeBytes": 182340,
"mimeType": "application/pdf",
"status": "active",
"accessCount": 0,
"createdAt": "2026-11-10T09:00:00.000Z",
"lastAccessedAt": null,
"revokedAt": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
adaLink | object | |
adaLink.id | string | |
adaLink.token | string | Secret part of the URL: share it only with the recipient. |
adaLink.url | string | Link sent to the recipient. |
adaLink.expiresAt | string | |
adaLink.recipientPhone | string | |
adaLink.channel | string | Values: sms |
adaLink.durationDays | integer | |
adaLink.costMad | string | Link price in MAD excl. VAT (SMS not included). |
adaLink.originalName | string | |
adaLink.fileSizeBytes | integer | |
adaLink.mimeType | string | |
adaLink.status | string | Values: active |
adaLink.accessCount | integer | |
adaLink.createdAt | string | |
adaLink.lastAccessedAt | string | null | |
adaLink.revokedAt | string | null |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 400 | — | Missing file field |
| 400 | — | File format not accepted |
| 400 | — | File larger than 10 MB |
| 400 | — | Unreadable number |
| 400 | NON_MOROCCAN_NUMBER | Number outside Morocco |
| 400 | — | durationDays missing or outside 1 to 30 |
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
| 402 | INSUFFICIENT_CREDIT | Insufficient balance |
| 500 | — | Link could not be created |
Send AdaLinks in bulk
/v1/adalink/bulkSends up to 200 documents from a ZIP archive. Each file is named after its recipient's number (e.g. 0600000001.pdf); an optional CSV file (columns fichier, telephone, duree) lets you set another number or a duration per document. Files that are not kept (format not accepted, unreadable or non-Moroccan number, duplicate) are ignored and counted in skipped. The price of every kept link is debited on acceptance; links are then created and sent in the background, and a link that fails is credited back.
Request body
multipart/form-data body.
| Field | Type | Required | Description |
|---|---|---|---|
documents | file | Yes | ZIP archive, 200 files at most, each file named after the recipient's number (e.g. 0600000001.pdf). |
mapping | file | — | Optional CSV, one line per file: fichier,telephone,duree (comma or semicolon separator, optional header line). The duration is optional. |
durationDays | integer | — | Default link duration, in days. Default: 7 |
Request
curl -X POST "https://api.adasms.ma/v1/adalink/bulk" \
-H "X-API-Key: adasms_YOUR_KEY" \
-F "documents=@invoices_november.zip" \
-F "durationDays=30"Response 202
Batch accepted; follow it with GET /adalink/bulk/{batchId}.
{
"success": true,
"batchId": "6b8d0f2a-4c5e-4a7b-9d1e-3f5a7b9c1d2e",
"totalFiles": 45,
"retained": 44,
"skipped": 1,
"warnings": [],
"estimatedCost": "858.0000"
}Response fields
| Field | Type | Description |
|---|---|---|
batchId | string | |
totalFiles | integer | |
retained | integer | Files kept and billed. |
skipped | integer | Ignored files (format, unreadable or non-Moroccan number, duplicate), not billed. |
warnings | array<string> | |
estimatedCost | string | Amount debited in MAD excl. VAT (links only, SMS not included). |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 400 | — | Missing documents field |
| 400 | — | The file is not a ZIP archive |
| 400 | — | durationDays outside 1 to 30 |
| 400 | — | No file kept |
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
| 402 | INSUFFICIENT_CREDIT | Insufficient balance |
| 403 | — | Account suspended |
| 500 | — | Link could not be created |
Retrieve an AdaLink batch
/v1/adalink/bulk/{batchId}Returns one of your AdaLink batches and the status of each kept document.
Path parameters
| Field | Type | Required | Description |
|---|---|---|---|
batchId | string | Yes | Identifier returned when sending (batchId). |
Request
curl "https://api.adasms.ma/v1/adalink/bulk/6b8d0f2a-4c5e-4a7b-9d1e-3f5a7b9c1d2e" \
-H "X-API-Key: adasms_YOUR_KEY"Response 200
The batch and its documents.
{
"success": true,
"batch": {
"id": "6b8d0f2a-4c5e-4a7b-9d1e-3f5a7b9c1d2e",
"clientId": "0b5e8f2a-1c3d-4e6f-9a7b-8c9d0e1f2a3b",
"channel": "sms",
"durationDays": 30,
"totalFiles": 45,
"successCount": 44,
"failedCount": 0,
"skippedCount": 1,
"status": "completed",
"createdAt": "2026-11-10T09:00:00.000Z",
"completedAt": "2026-11-10T09:02:10.000Z"
},
"items": [
{
"id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"fileName": "0600000001.pdf",
"phone": "+212600000001",
"durationDays": 30,
"status": "sent",
"errorReason": null,
"adaLinkId": "2d4f6a8c-0e1b-4c3d-8e5f-7a9b0c1d2e3f"
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
batch | object | |
batch.id | string | |
batch.clientId | string | |
batch.channel | string | Values: sms |
batch.durationDays | integer | |
batch.totalFiles | integer | |
batch.successCount | integer | |
batch.failedCount | integer | |
batch.skippedCount | integer | |
batch.status | string | Values: pending, processing, completed, failed |
batch.createdAt | string | |
batch.completedAt | string | null | |
items | array<object> | |
items[].id | string | |
items[].fileName | string | |
items[].phone | string | |
items[].durationDays | integer | |
items[].status | string | Values: pending, sent, failed |
items[].errorReason | string | null | |
items[].adaLinkId | string | null |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
| 404 | — | Batch not found |
Account balance
/v1/accountReturns your account's balance, plan, billing mode and the price of one SMS segment.
Subject to rate limiting. See ›
Request
curl "https://api.adasms.ma/v1/account" \
-H "X-API-Key: adasms_YOUR_KEY"Response 200
Account information.
{
"success": true,
"account": {
"name": "My Company",
"plan": "starter",
"billingMode": "prepaid",
"balance": "549.8100",
"currency": "MAD",
"smsUnitPrice": "0.4900",
"estimatedSmsRemaining": 1122
}
}Response fields
| Field | Type | Description |
|---|---|---|
account | object | |
account.name | string | |
account.plan | string | Plan code (starter, business, enterprise). |
account.billingMode | string | Values: prepaid, postpaid |
account.balance | string | Balance in MAD excl. VAT, 4 decimals, same as the dashboard. |
account.currency | string | Values: MAD |
account.smsUnitPrice | string | Price of one SMS segment on your plan, in MAD excl. VAT. |
account.estimatedSmsRemaining | integer | null | Number of one-segment GSM-7 SMS the balance covers (integer part of balance ÷ smsUnitPrice); 0 if the balance is zero or negative; null for postpaid accounts. |
Errors
| HTTP status | Code | Cause |
|---|---|---|
| 401, 403 | — | Missing or invalid API key, suspended account, key's Sender ID expired: see Authentication. |
| 429 | RATE_LIMITED | Rate limit reached |
Webhooks
A webhook notifies your server of an event (SMS sent, SMS failed, bulk send or campaign finished) with a JSON POST request. Create it in your customer area, Webhooks menu: an HTTPS receiving URL and the events you want. The signing secret is shown only once, when the webhook is created (or when you generate a new one).
Headers sent
| Header | Value |
|---|---|
Content-Type | application/json |
X-AdaSMS-Event | Event name, for example message.sent |
X-AdaSMS-Timestamp | Time the request was sent (ISO 8601) |
X-AdaSMS-Signature | sha256= followed by the HMAC-SHA256 signature of the raw body, computed with your secret |
Verify the signature (Node.js)
const crypto = require('crypto');
const express = require('express');
const app = express();
function isValidSignature(rawBody, secret, header) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return header.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
// Read the raw body: the signature covers the bytes received.
app.post('/webhooks/adasms', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-AdaSMS-Signature') || '';
if (!isValidSignature(req.body, process.env.ADASMS_WEBHOOK_SECRET, header)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
console.log(event.event, event.data);
res.sendStatus(200);
});Verify the signature (PHP)
<?php
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_ADASMS_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $body, getenv('ADASMS_WEBHOOK_SECRET'));
if (!hash_equals($expected, $header)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
http_response_code(200);Verify the signature (Python)
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post('/webhooks/adasms')
def webhook():
expected = 'sha256=' + hmac.new(os.environ['ADASMS_WEBHOOK_SECRET'].encode(),
request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get('X-AdaSMS-Signature', '')):
abort(401)
event = request.get_json()
return '', 200Response and retries
Reply with a 2xx status within 10 seconds. Otherwise (error, timeout), AdaSMS.ma sends the same event again 1 minute and then 5 minutes later: 3 attempts in total. Process events idempotently (for example with messageId), as the same event may arrive twice.
Events
| Event | Sent when |
|---|---|
message.sent | An SMS sent with POST /messages has been handed to the operator. |
message.failed | An SMS sent with POST /messages was rejected; its amount has been credited back (cost is "0.0000"). |
batch.completed | A bulk send started from the dashboard (or a campaign) has finished. Batches sent with POST /bulk are processed during the request: they do not trigger this webhook, as the response already contains their result. |
campaign.completed | A campaign has finished (status completed) or was stopped while sending (status stopped). |
campaign.failed | A scheduled campaign could not start at its scheduled time (for example insufficient balance); nothing was debited. |
message.sent — SMS handed to the operator
{
"event": "message.sent",
"timestamp": "2026-11-10T09:00:01.000Z",
"data": {
"messageId": "3f6c2a91-5b7e-4d0a-9c1e-2b8f4e6d7a10",
"to": "+212600000001",
"from": "MyService",
"type": "otp",
"channel": "sms",
"status": "sent",
"cost": "0.4900",
"segments": 1,
"encoding": "GSM7",
"reference": "CMD-2026-001",
"billingOutcome": "charged",
"providerMessageId": "9a8b7c6d-0000-4e1f-8a2b-3c4d5e6f7a8b",
"clientTxnId": null,
"networkErrorCode": null,
"errorMessage": null,
"batchId": null,
"campaignId": null
}
}message.failed — SMS failed
{
"event": "message.failed",
"timestamp": "2026-11-10T09:00:01.000Z",
"data": {
"messageId": "3f6c2a91-5b7e-4d0a-9c1e-2b8f4e6d7a11",
"to": "+212600000002",
"from": "MyService",
"type": "transactional",
"channel": "sms",
"status": "failed",
"cost": "0.0000",
"segments": 1,
"encoding": "GSM7",
"reference": null,
"billingOutcome": "not_charged",
"providerMessageId": null,
"clientTxnId": null,
"networkErrorCode": null,
"errorMessage": "Recipient rejected",
"batchId": null,
"campaignId": null
}
}batch.completed — Bulk send finished
{
"event": "batch.completed",
"timestamp": "2026-11-10T09:05:00.000Z",
"data": {
"batchId": "7c1d9e20-4a3b-4f6e-8d2c-1a0b9c8d7e70",
"totalMessages": 250,
"sentCount": 248,
"failedCount": 2,
"actualCost": "121.5200",
"status": "completed",
"campaignId": null
}
}campaign.completed — Campaign finished
{
"event": "campaign.completed",
"timestamp": "2026-11-10T10:00:00.000Z",
"data": {
"campaignId": "4e6a8c0e-2b3d-4f5a-9b7c-1d3e5f7a9b0c",
"totalMessages": 500,
"sentCount": 497,
"failedCount": 3,
"actualCost": "243.5300",
"status": "completed"
}
}campaign.failed — Scheduled campaign not started
{
"event": "campaign.failed",
"timestamp": "2026-11-10T08:00:00.000Z",
"data": {
"campaignId": "4e6a8c0e-2b3d-4f5a-9b7c-1d3e5f7a9b0d",
"name": "November promo",
"scheduledAt": "2026-11-10T08:00:00.000Z",
"error": "Solde insuffisant : coût 243.5300 MAD, solde 12.0000 MAD"
}
}Error codes
Stable values of the code field, to use in your code. Other errors have no code: rely on the HTTP status.
| Code | HTTP status | Description |
|---|---|---|
NON_MOROCCAN_NUMBER | 400 | The number is not a Moroccan number (+212 followed by 9 digits). Nothing is debited. |
TEXT_TOO_LONG | 400 | The text is longer than 10 segments (1,530 GSM-7 characters or 670 in Unicode). |
DEFAULT_SENDER_FORBIDDEN | 400 | You have an approved Sender ID, so the shared sender is no longer available; set from. |
DEFAULT_SENDER_QUOTA_EXCEEDED | 400 | Your trial messages with the shared sender are used up; request your Sender ID. |
INSUFFICIENT_CREDIT | 402 | Insufficient balance. Nothing is debited; top up your account. |
EMAIL_NOT_VERIFIED | 403 | The account's email address is not confirmed; sending is blocked until it is. |
SENDER_EXPIRED | 403 | The API key is linked to an expired or inactive Sender ID; renew it or change the key. |
RATE_LIMITED | 429 | Too many requests; wait for the delay given in the Retry-After header. |
Pricing
Prepaid credit, with no commitment or subscription. The price of an SMS depends on your plan and is counted per segment (see Segments and encoding). The amount is debited when sending and credited back if the operator rejects the message.
| Plan | Price per segment | Volume |
|---|---|---|
| Starter | 0.49 MAD excl. VAT | fewer than 10,000 SMS/month |
| Business | 0.35 MAD excl. VAT | 10,000 to 99,999 SMS/month |
| Enterprise | 0.30 MAD excl. VAT | 100,000+ SMS/month |
AdaLink
Price per link, based on how long it stays valid: each day is billed at its band's rate. The SMS carrying the link is charged on top, at your plan's price.
| Validity | Price per day excl. VAT | Example |
|---|---|---|
| 1 to 7 days | 1.40 MAD | 1 d = 1.40 · 7 d = 9.80 MAD |
| 8 to 14 days | 0.70 MAD | 14 d = 14.70 MAD |
| 15 to 30 days | 0.30 MAD | 30 d = 19.50 MAD |
| More than 30 days | On quotation | — |
GET /account returns the price of one segment on your plan (smsUnitPrice) and your balance.
Quick start
- Create your account on app.adasms.ma and confirm your email address: sending is blocked until it is confirmed.
- Create an API key in the API Keys menu and copy it (it will not be shown again).
- Send your first SMS:
curl -X POST https://api.adasms.ma/v1/messages \
-H "X-API-Key: adasms_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": "0600000001", "text": "Hello from AdaSMS.ma"}'A 202 response with "success": true confirms the send. Without an approved Sender ID, leave from empty: the shared sender is used for your trial messages. To send under your own name, request a Sender ID from your customer area.
In your customer area only
Your API keys, webhook secrets, message history, invoices and Sender IDs are managed in your customer area, app.adasms.ma. A question? support@adasms.ma.
