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.

Production base URLhttps://api.smsmaroc.ma
Staging base URLhttps://api-staging.smsmaroc.ma (configured in wrangler, not verified live)
Version prefix/v1
Content typeapplication/json; charset=utf-8
AuthenticationAuthorization: Bearer smr_live_xxx
Default rate limit100 requests per minute per API key
Phone formatE.164, for example +212612345678
Supported channelssms in current production; whatsapp and telegram require dedicated upstream endpoints before use
01

Create an API key in Dashboard > API Keys and store the full key once.

02

Send API calls with Bearer authentication and JSON request bodies.

03

Use sandbox keys first; sandbox OTP responses include sandbox_code for tests.

04

Store message ids and verify session ids returned by SMS Maroc.

05

Register a signed webhook endpoint before going live.

06

Monitor balance, delivery status, and error codes from your dashboard or API.

Send your first message
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

Authorization: Bearer smr_live_xxx

Response headers

X-RateLimit-Limit and X-RateLimit-Remaining

IP allowlists

Requests outside the configured IP list return 401.

CORS

JSON, Authorization, X-SMSMaroc-Signature, and X-SMSMaroc-Version are allowed headers.

Version header

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.

Example response
{
  "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.

Request body
  • 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.
Notes
  • Returns 202 when queued.
  • SMS cost is multiplied by segment count.
  • Unsupported channels return CHANNEL_NOT_CONFIGURED before a message is queued.
Example response
{
  "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.

Request body
  • 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.
Notes
  • Maximum batch size is 10,000 messages.
  • The full batch is rejected if balance is insufficient.
Example response
{
  "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.

Query parameters
  • 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.
Example response
{
  "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.

Example response
{
  "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.

Request body
  • 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.
Notes
  • Sandbox environments include sandbox_code in the response.
Example 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.

Request body
  • session_id: value returned by /v1/verify/send.
  • code: user-entered OTP code.
Notes
  • Sessions allow 3 failed attempts before returning MAX_ATTEMPTS.
Example response
{
  "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.

Example response
{
  "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.

Query parameters
  • days: number of days to include. Defaults to 30, maximum 90.
Example response
{
  "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.

Query parameters
  • q: search phone, name, or email.
  • list_id: return members of one list.
  • limit: default 100, maximum 500.
  • offset: default 0.
Example response
{
  "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.

Request body
  • phone: required.
  • name: optional.
  • email: optional.
  • list_id: optional list to attach the contact to.
  • custom_fields: custom1, custom2, and custom3.
Example response
{
  "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.

Request body
  • contacts: array of phone, name, and email objects.
  • list_id: optional destination list.
Example response
{
  "imported": 4998,
  "skipped": 2
}
GET/v1/contacts/listsList contact listsRequired

Returns all contact lists for the account.

Example response
{
  "data": [{ "id": "lst_...", "name": "Customers", "count": 1842 }]
}
POST/v1/contacts/listsCreate contact listRequired

Creates a reusable audience list.

Request body
  • name: required.
  • description: optional.
Example response
{
  "id": "lst_...",
  "name": "Customers",
  "description": "Main active customers",
  "count": 0
}
DELETE/v1/contacts/{id}Delete contactRequired

Deletes a contact from the authenticated account.

Example response
{
  "deleted": true,
  "id": "ctc_..."
}
GET/v1/optoutsList opt-outsRequired

Returns recipients who opted out for the authenticated account.

Query parameters
  • limit: default 100, maximum 500.
  • offset: default 0.
Example response
{
  "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.

Request body
  • phone: required recipient number.
Example response
{
  "phone": "+212612345678",
  "opted_out": true
}
DELETE/v1/optouts/{phone}Remove opt-outRequired

Removes a phone number from the opt-out registry.

Example response
{
  "phone": "+212612345678",
  "opted_out": false
}

Campaigns

GET/v1/campaignsList campaignsRequired

Returns campaigns ordered by creation date.

Example response
{
  "data": [{ "id": "cmp_...", "name": "Ramadan promo", "status": "draft" }]
}
POST/v1/campaignsCreate campaignRequired

Creates a draft or scheduled campaign for a contact list.

Request body
  • 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.
Example response
{
  "id": "cmp_...",
  "status": "draft"
}
GET/v1/campaigns/{id}Get campaignRequired

Returns one campaign and its counters.

Example response
{
  "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.

Notes
  • A campaign must have a list_id before it can be sent.
  • The send operation queues at most 10,000 contacts.
Example response
{
  "id": "cmp_...",
  "queued": 4980,
  "total": 5000
}

Webhooks

GET/v1/webhooksList webhooksRequired

Lists active and inactive webhook endpoints.

Example response
{
  "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.

Request body
  • 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.
Example response
{
  "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.

Request body
  • url: optional HTTPS URL.
  • events: optional event array.
  • active: optional boolean.
Example response
{
  "updated": true,
  "id": "whk_..."
}
DELETE/v1/webhooks/{id}Disable webhookRequired

Soft-deletes a webhook by setting active to false.

Example response
{
  "deleted": true,
  "id": "whk_..."
}
POST/v1/webhooks/{id}/testTest webhookRequired

Queues a message.test webhook delivery.

Example response
{
  "queued": true,
  "id": "whk_..."
}

API Keys

GET/v1/api-keysList API keysRequired

Lists API keys without exposing full secret values.

Example response
{
  "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.

Request body
  • name: optional key label.
  • permissions: optional array of permissions.
  • ip_whitelist: optional array of allowed IPs.
  • rate_limit: 10 to 1000 requests per minute.
Example response
{
  "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.

Request body
  • name: optional.
  • permissions: optional.
  • ip_whitelist: optional.
  • rate_limit: optional.
  • active: optional boolean.
Example response
{
  "updated": true,
  "id": "key_..."
}
DELETE/v1/api-keys/{id}Revoke API keyRequired

Revokes an API key by setting active to false.

Example response
{
  "revoked": true,
  "id": "key_..."
}

Billing

GET/v1/billing/balanceGet billing balanceRequired

Returns account balance and active plan.

Example response
{
  "balance_eur": 45.23,
  "balance_mad": 497.53,
  "currency": "EUR",
  "plan": "business"
}
GET/v1/balanceBalance aliasRequired

Compatibility alias for /v1/billing/balance.

Example response
{
  "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.

Query parameters
  • limit: default 50, maximum 200.
  • offset: default 0.
Example response
{
  "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.

Example response
{
  "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.

Request body
  • amount_eur: required. Minimum 10.
  • payment_method: stripe, cmi, bank_transfer, or another enabled method. Defaults to stripe.
Example response
{
  "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_length from 4 to 8 digits.
  • Use expiry from 60 to 1800 seconds.
  • Use template with {{code}} and {{brand}} placeholders.
  • Handle INVALID_CODE, MAX_ATTEMPTS, and SESSION_NOT_FOUND.
OTP integration
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.sent

Queued job was accepted by the upstream or sandbox transport.

message.delivered

SMS.to or the configured provider confirmed delivery.

message.failed

The message failed after available routing attempts.

message.undeliverable

The provider rejected, expired, or marked the message unreachable.

message.test

Synthetic delivery generated by the test endpoint.

message.*

Wildcard subscription for all message events.

Webhook setup
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_JSON

Request body could not be parsed as JSON.

UNAUTHORIZED

API key is missing, malformed, revoked, inactive, or IP restricted.

RATE_LIMITED

The per-minute API key limit was exceeded. Retry after 60 seconds.

MISSING_FIELD

A required field such as to, message, name, or session_id is missing.

INVALID_PHONE

Phone number cannot be normalized to valid E.164 format.

INVALID_CHANNEL

channel must be sms, whatsapp, or telegram.

CHANNEL_NOT_CONFIGURED

The requested channel is not configured in the current environment. Production currently supports sms.

INSUFFICIENT_BALANCE

Balance is too low for the requested message or batch.

OPTED_OUT

The recipient opted out for this account.

SESSION_NOT_FOUND

OTP session does not exist, expired, or belongs to another account.

INVALID_CODE

OTP code does not match the active session.

MAX_ATTEMPTS

OTP session reached the maximum number of failed checks.

TOO_MANY_MESSAGES

Bulk request exceeded 10,000 messages.

TOO_MANY_CONTACTS

Contact import exceeded 5,000 contacts.

NOT_FOUND

The requested resource does not exist for this account.

Limits and behavior

Production constraints that affect integration design.

Download OpenAPI YAML
Message length

1600 characters after sanitization.

SMS segmentation

160 GSM chars for one segment; 153 per segment after concatenation. Unicode uses 70 then 67.

Bulk send

10,000 messages per request.

Contact import

5,000 contacts per request.

Message list page

200 maximum.

Contact list page

500 maximum.

OTP code length

4 to 8 digits.

OTP expiry

60 to 1800 seconds.

OTP attempts

3 failed checks.

Webhook URL

HTTPS only.