API Reference
Complete API documentation for integrating SMS Maroc
Quickstart, authentication, REST endpoints, OTP, signed webhooks, errors, limits, and OpenAPI are documented for sandbox-to-production launches.
Start here
Everything a developer needs before the first request.
Create an API key in Dashboard > API Keys and store the full key once.
Send API calls with Bearer authentication and JSON request bodies.
Use sandbox keys first; sandbox OTP responses include sandbox_code for tests.
Store message ids and verify session ids returned by SMS Maroc.
Register a signed webhook endpoint before going live.
Monitor balance, delivery status, and error codes from your dashboard or API.
curl -X POST https://api.smsmaroc.ma/v1/messages \
-H "Authorization: Bearer smr_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"message": "Votre code est 483921",
"from": "MonApp"
}' OpenAPI YAML Authentication
Bearer keys, rate-limit headers, and account scoping.
Every /v1 endpoint requires an API key. Keys are hashed server-side, can be IP restricted, and inherit the account plan, balance, and per-minute rate limit.
Authorization: Bearer smr_live_xxx
X-RateLimit-Limit and X-RateLimit-Remaining
Requests outside the configured IP list return 401.
JSON, Authorization, X-SMSMaroc-Signature, and X-SMSMaroc-Version are allowed headers.
Responses include X-SMSMaroc-Version: 1.0.
Endpoint reference
36 implemented endpoints, matched to the Worker API.
System
GET/healthHealth checkNone
Returns service health, current API version, and server time.
{
"status": "ok",
"service": "smsmaroc-api",
"version": "1.0.0",
"time": "2026-04-30T10:30:00.000Z"
}Messages
POST/v1/messagesSend one messageRequired
Queues one SMS message through the active SMS.to adapter.
- to: Moroccan phone number. Local 06/07, 212, and +212 inputs are normalized to E.164.
- message or body: message text. Control characters are removed and text is capped at 1600 characters.
- from: optional sender id. Defaults to SMSMAROC.
- channel: sms. Defaults to sms. whatsapp and telegram return CHANNEL_NOT_CONFIGURED unless dedicated upstream endpoints are configured.
- cascade: when true, SMS Maroc uses only configured channels; current production fallback order is sms.
- metadata: optional JSON object stored with the message.
- Returns 202 when queued.
- SMS cost is multiplied by segment count.
- Unsupported channels return CHANNEL_NOT_CONFIGURED before a message is queued.
{
"id": "msg_4f7c2c2a1a...",
"to": "+212612345678",
"channel": "sms",
"cascade": false,
"status": "queued",
"cost": { "eur": 0.22, "mad": 2.42 },
"segments": 1,
"created_at": "2026-04-30T10:30:00.000Z"
}POST/v1/messages/bulkSend a bulk batchRequired
Queues up to 10,000 personalized messages in one request.
- messages: array of objects with to, message/body, and optional metadata.
- from: optional sender id used for every message.
- channel: sms. Defaults to sms. Future channels require dedicated upstream endpoints.
- Maximum batch size is 10,000 messages.
- The full batch is rejected if balance is insufficient.
{
"batch_id": "batch_8d7e...",
"total": 2,
"channel": "sms",
"estimated_cost": { "eur": 0.44, "mad": 4.84 },
"messages": [
{ "id": "msg_...", "to": "+212612345678", "status": "queued" }
]
}GET/v1/messagesList messagesRequired
Returns recent messages for the authenticated account.
- status: queued, sent, delivered, failed, or undeliverable.
- channel: sms, or a future configured channel.
- from_date: ISO date/time lower bound.
- to_date: ISO date/time upper bound.
- limit: default 50, maximum 200.
- offset: default 0.
{
"data": [{ "id": "msg_...", "status": "delivered" }],
"limit": 50,
"offset": 0,
"total": 1
}GET/v1/messages/{id}Get message statusRequired
Returns a single message, including channel, status, costs, timestamps, and upstream id when available.
{
"id": "msg_...",
"to": "+212612345678",
"channel": "sms",
"body": "Votre code est 483921",
"status": "delivered",
"segments": 1,
"cost_eur": 0.22,
"sent_at": "2026-04-30T10:30:02Z",
"delivered_at": "2026-04-30T10:30:05Z"
}Verify
POST/v1/verify/sendSend OTPRequired
Creates an OTP session and sends the code over SMS.
- to: recipient phone number.
- channel: sms. Defaults to sms. Future channels require dedicated upstream endpoints.
- code_length: 4 to 8 digits. Defaults to 6.
- expiry: 60 to 1800 seconds. Defaults to 600.
- brand: displayed in the message, capped at 32 characters.
- template: optional text using {{code}} and {{brand}} placeholders.
- Sandbox environments include sandbox_code in the response.
{
"session_id": "vrf_...",
"message_id": "msg_...",
"to": "+212612345678",
"channel": "sms",
"expires_at": "2026-04-30T10:40:00.000Z",
"status": "sent"
}POST/v1/verify/checkCheck OTPRequired
Validates a code for an active verification session.
- session_id: value returned by /v1/verify/send.
- code: user-entered OTP code.
- Sessions allow 3 failed attempts before returning MAX_ATTEMPTS.
{
"session_id": "vrf_...",
"verified": true,
"verified_at": "2026-04-30T10:34:00.000Z"
}GET/v1/verify/{session_id}Get OTP sessionRequired
Returns non-secret OTP session state for audit and support screens.
{
"id": "vrf_...",
"to": "+212612345678",
"channel": "sms",
"expires_at": "2026-04-30T10:40:00Z",
"verified_at": null,
"created_at": "2026-04-30T10:30:00Z"
}Analytics
GET/v1/analyticsUsage analyticsRequired
Returns totals, delivery rate, costs, configured channel usage, and daily volume for the last window.
- days: number of days to include. Defaults to 30, maximum 90.
{
"window_days": 30,
"summary": {
"total": 12840,
"delivered": 12570,
"failed": 94,
"delivery_rate": 97.9,
"cost_eur": 242.10,
"cost_mad": 2663.10
},
"channels": [],
"daily": []
}Contacts
GET/v1/contactsList contactsRequired
Lists contacts, optionally filtered by search text or list id.
- q: search phone, name, or email.
- list_id: return members of one list.
- limit: default 100, maximum 500.
- offset: default 0.
{
"data": [{ "id": "ctc_...", "phone": "+212612345678", "name": "Sara" }],
"limit": 100,
"offset": 0,
"total": 1
}POST/v1/contactsCreate or update contactRequired
Upserts one contact by account and phone number.
- phone: required.
- name: optional.
- email: optional.
- list_id: optional list to attach the contact to.
- custom_fields: custom1, custom2, and custom3.
{
"id": "ctc_...",
"phone": "+212612345678",
"name": "Sara",
"email": "sara@example.com"
}POST/v1/contacts/importImport contactsRequired
Imports up to 5,000 contacts and optionally attaches them to a list.
- contacts: array of phone, name, and email objects.
- list_id: optional destination list.
{
"imported": 4998,
"skipped": 2
}GET/v1/contacts/listsList contact listsRequired
Returns all contact lists for the account.
{
"data": [{ "id": "lst_...", "name": "Customers", "count": 1842 }]
}POST/v1/contacts/listsCreate contact listRequired
Creates a reusable audience list.
- name: required.
- description: optional.
{
"id": "lst_...",
"name": "Customers",
"description": "Main active customers",
"count": 0
}DELETE/v1/contacts/{id}Delete contactRequired
Deletes a contact from the authenticated account.
{
"deleted": true,
"id": "ctc_..."
}GET/v1/optoutsList opt-outsRequired
Returns recipients who opted out for the authenticated account.
- limit: default 100, maximum 500.
- offset: default 0.
{
"data": [{ "phone": "+212612345678", "opted_out_at": "2026-04-30T10:30:00Z" }],
"limit": 100,
"offset": 0,
"total": 1
}POST/v1/optoutsCreate opt-outRequired
Adds a phone number to the account opt-out registry and marks the matching contact as opted out.
- phone: required recipient number.
{
"phone": "+212612345678",
"opted_out": true
}DELETE/v1/optouts/{phone}Remove opt-outRequired
Removes a phone number from the opt-out registry.
{
"phone": "+212612345678",
"opted_out": false
}Campaigns
GET/v1/campaignsList campaignsRequired
Returns campaigns ordered by creation date.
{
"data": [{ "id": "cmp_...", "name": "Ramadan promo", "status": "draft" }]
}POST/v1/campaignsCreate campaignRequired
Creates a draft or scheduled campaign for a contact list.
- name: required.
- channel: sms. Defaults to sms. Future channels require dedicated upstream endpoints.
- list_id: optional list id.
- template_id: optional template id.
- body: required unless template_id is provided. Supports {{name}} when sent.
- sender_id: optional sender. Defaults to SMSMAROC.
- scheduled_at: optional ISO date/time; creates scheduled status.
{
"id": "cmp_...",
"status": "draft"
}GET/v1/campaigns/{id}Get campaignRequired
Returns one campaign and its counters.
{
"id": "cmp_...",
"name": "Ramadan promo",
"status": "running",
"total_count": 5000,
"sent_count": 4980
}POST/v1/campaigns/{id}/sendSend campaignRequired
Queues messages for all non-opted-out contacts in the campaign list.
- A campaign must have a list_id before it can be sent.
- The send operation queues at most 10,000 contacts.
{
"id": "cmp_...",
"queued": 4980,
"total": 5000
}Webhooks
GET/v1/webhooksList webhooksRequired
Lists active and inactive webhook endpoints.
{
"data": [{
"id": "whk_...",
"url": "https://example.com/smsmaroc/webhook",
"events": "[\"message.delivered\",\"message.failed\"]",
"active": 1
}]
}POST/v1/webhooksCreate webhookRequired
Registers an HTTPS endpoint for signed delivery callbacks.
- url: required HTTPS URL.
- events: optional array. Defaults to message.delivered and message.failed.
- secret: optional HMAC secret. If omitted, SMS Maroc generates one and returns it once.
{
"id": "whk_...",
"url": "https://example.com/smsmaroc/webhook",
"events": ["message.delivered", "message.failed"],
"secret": "sec_...",
"active": true
}PATCH/v1/webhooks/{id}Update webhookRequired
Updates URL, subscribed events, or active status.
- url: optional HTTPS URL.
- events: optional event array.
- active: optional boolean.
{
"updated": true,
"id": "whk_..."
}DELETE/v1/webhooks/{id}Disable webhookRequired
Soft-deletes a webhook by setting active to false.
{
"deleted": true,
"id": "whk_..."
}POST/v1/webhooks/{id}/testTest webhookRequired
Queues a message.test webhook delivery.
{
"queued": true,
"id": "whk_..."
}API Keys
GET/v1/api-keysList API keysRequired
Lists API keys without exposing full secret values.
{
"data": [{
"id": "key_...",
"name": "Production key",
"key_prefix": "smr_abc12345",
"permissions": "[\"send\",\"status\"]",
"active": 1
}]
}POST/v1/api-keysCreate API keyRequired
Creates a scoped API key and returns the full key once.
- name: optional key label.
- permissions: optional array of permissions.
- ip_whitelist: optional array of allowed IPs.
- rate_limit: 10 to 1000 requests per minute.
{
"id": "key_...",
"name": "Production key",
"api_key": "smr_live_secret_shown_once",
"permissions": ["send", "status"],
"rate_limit": 100
}PATCH/v1/api-keys/{id}Update API keyRequired
Updates label, permissions, IP allowlist, rate limit, or active status.
- name: optional.
- permissions: optional.
- ip_whitelist: optional.
- rate_limit: optional.
- active: optional boolean.
{
"updated": true,
"id": "key_..."
}DELETE/v1/api-keys/{id}Revoke API keyRequired
Revokes an API key by setting active to false.
{
"revoked": true,
"id": "key_..."
}Billing
GET/v1/billing/balanceGet billing balanceRequired
Returns account balance and active plan.
{
"balance_eur": 45.23,
"balance_mad": 497.53,
"currency": "EUR",
"plan": "business"
}GET/v1/balanceBalance aliasRequired
Compatibility alias for /v1/billing/balance.
{
"balance_eur": 45.23,
"balance_mad": 497.53,
"currency": "EUR",
"plan": "business"
}GET/v1/billing/transactionsList billing transactionsRequired
Returns usage, top-up, refund, and adjustment transactions.
- limit: default 50, maximum 200.
- offset: default 0.
{
"data": [{ "id": "txn_...", "type": "usage", "amount_eur": -0.22 }],
"limit": 50,
"offset": 0,
"total": 1
}GET/v1/billing/plansList plan pricingRequired
Returns pricing rows by plan. Current live sending is SMS until extra channel endpoints are configured.
{
"data": [
{ "plan": "starter", "channel": "sms", "price_eur": 0.22, "price_mad": 2.42 }
]
}POST/v1/billing/topupsCreate top-upRequired
Creates a pending account top-up and returns checkout_url.
- amount_eur: required. Minimum 10.
- payment_method: stripe, cmi, bank_transfer, or another enabled method. Defaults to stripe.
{
"id": "topup_...",
"status": "pending",
"amount_eur": 50,
"amount_mad": 550,
"checkout_url": "https://smsmaroc.ma/dashboard/billing?topup=topup_..."
}OTP flow
Send, store the session id, then verify the code.
OTP sessions are backed by short-lived KV storage and an audit row. The code itself is hashed, never returned outside sandbox mode, and verification succeeds once before the session is deleted.
- Use
code_lengthfrom 4 to 8 digits. - Use
expiryfrom 60 to 1800 seconds. - Use
templatewith{{code}}and{{brand}}placeholders. - Handle
INVALID_CODE,MAX_ATTEMPTS, andSESSION_NOT_FOUND.
curl -X POST https://api.smsmaroc.ma/v1/verify/send \
-H "Authorization: Bearer smr_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"channel": "sms",
"brand": "MonApp",
"code_length": 6,
"expiry": 600
}' OpenAPI YAML Webhooks
Signed delivery callbacks for production systems.
SMS Maroc posts JSON callbacks with X-SMSMaroc-Signature, X-SMSMaroc-Event, and a ten-second delivery timeout.
message.sentQueued job was accepted by the upstream or sandbox transport.
message.deliveredSMS.to or the configured provider confirmed delivery.
message.failedThe message failed after available routing attempts.
message.undeliverableThe provider rejected, expired, or marked the message unreachable.
message.testSynthetic delivery generated by the test endpoint.
message.*Wildcard subscription for all message events.
curl -X POST https://api.smsmaroc.ma/v1/webhooks \
-H "Authorization: Bearer smr_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/smsmaroc/webhook",
"events": ["message.sent", "message.delivered", "message.failed"],
"secret": "whsec_keep_this_private"
}' OpenAPI YAML Errors
Predictable JSON errors with docs links.
All API errors use the same envelope, so integrations can branch on error.code.
{
"error": {
"code": "INVALID_PHONE",
"message": "to must be E.164 format, for example +212612345678",
"docs": "https://smsmaroc.ma/en/docs#errors"
}
}INVALID_JSONRequest body could not be parsed as JSON.
UNAUTHORIZEDAPI key is missing, malformed, revoked, inactive, or IP restricted.
RATE_LIMITEDThe per-minute API key limit was exceeded. Retry after 60 seconds.
MISSING_FIELDA required field such as to, message, name, or session_id is missing.
INVALID_PHONEPhone number cannot be normalized to valid E.164 format.
INVALID_CHANNELchannel must be sms, whatsapp, or telegram.
CHANNEL_NOT_CONFIGUREDThe requested channel is not configured in the current environment. Production currently supports sms.
INSUFFICIENT_BALANCEBalance is too low for the requested message or batch.
OPTED_OUTThe recipient opted out for this account.
SESSION_NOT_FOUNDOTP session does not exist, expired, or belongs to another account.
INVALID_CODEOTP code does not match the active session.
MAX_ATTEMPTSOTP session reached the maximum number of failed checks.
TOO_MANY_MESSAGESBulk request exceeded 10,000 messages.
TOO_MANY_CONTACTSContact import exceeded 5,000 contacts.
NOT_FOUNDThe requested resource does not exist for this account.
Limits and behavior
Production constraints that affect integration design.
1600 characters after sanitization.
160 GSM chars for one segment; 153 per segment after concatenation. Unicode uses 70 then 67.
10,000 messages per request.
5,000 contacts per request.
200 maximum.
500 maximum.
4 to 8 digits.
60 to 1800 seconds.
3 failed checks.
HTTPS only.