Start here
Everything to get started before your first request.
EnvoiSMS.ma is a programmable messaging infrastructure built for Morocco. Our local routes to IAM, Inwi, and Orange, with automatic failover between routes, keep latency low.
Test live endpoints, explore full OpenAPI 3.1 payload schemas, and generate cURL, PHP, Node.js, Python, and Go snippets instantly.
Generate your "smr_..." API key from your EnvoiSMS.ma Console.
Use Bearer authentication in your HTTP headers for every request.
Test your integration with Sandbox keys (env_test_...) on the live URL to validate your calls without spending real credit.
Integrate the WhatsApp Business API to divide your OTP costs by 10.
Configure a signed Webhook to receive delivery reports in real time.
Track your consumption and MAD invoices directly on your Dashboard.
// config/services.php
'envoisms' => [
'key' => env('ENVOISMS_API_KEY'),
],
// Usage
Http::withToken(config('services.envoisms.key'))
->post('https://api.envoisms.ma/v1/messages', [
'to' => '+212612345678',
'message' => 'Votre commande est en cours de livraison 🚚',
'from' => 'MaBoutique'
]); OpenAPI Spec Authentication
Bearer keys, headers, and security constraints.
Each /v1 endpoint requires a valid API key passed in the Authorization header as a Bearer token. Test and production keys can be generated or revoked from your console.
Authorization: Bearer smr_xxx
X-RateLimit-Limit and X-RateLimit-Remaining returned with each call.
Requests coming from unconfigured IP addresses return a 401 status.
JSON, Authorization, X-EnvoiSMS.ma-Signature, and X-EnvoiSMS.ma-Version are supported.
POST /v1/messages HTTP/1.1
Host: api.envoisms.ma
Authorization: Bearer smr_xxxxxxxx
Content-Type: application/json
Accept: application/jsonOpen Source
Official SDKs & Client Libraries
Integrate EnvoiSMS API into your application with our official open-source client libraries.
npm install envoismscomposer require envoisms/envoisms-phppip install envoismscomposer require envoisms/laravel-otpgo get github.com/envoisms/envoisms-gonpx -y @envoisms/mcp-serverSend a message
Sends an SMS or a WhatsApp Business message to a single recipient.
| Field | Type | Requirement | Description |
|---|---|---|---|
| to | string | Required | Phone number in E.164 format (+212...). |
| message | string | Required | Text content (max 1600 characters). Also accepted under the name "body". |
| from | string | Optional | Custom Sender ID (e.g. BRAND_NAME). Defaults to "EnvoiSMS". |
| channel | string | Optional | "sms" or "whatsapp". Defaults to "sms". "whatsapp" sends from your own connected WhatsApp Business number (403 WHATSAPP_NOT_CONNECTED otherwise). To send an ordinary text to a customer who uses WhatsApp, "sms" is enough: no connection, no template, no 24-hour window. Unless it is a template, a WhatsApp message is only accepted if the contact wrote to you in the last 24 hours — otherwise 400 OUT_OF_24H_WINDOW, with nothing charged (with cascade, WhatsApp is skipped for the next channel). |
| cascade | boolean | Optional | If enabled, tries WhatsApp, then falls back to SMS: at once if WhatsApp refuses the message or reports it failed (not on WhatsApp, 24-hour window closed, …), or when no delivery report arrives within cascade_timeout (120 s by default). Only a send that actually went out is billed: a refused or failed WhatsApp message is not, and you pay for the SMS alone. A WhatsApp message still waiting for its delivery report did go out (it can still be delivered when the phone reconnects), so it is billed along with its SMS fallback, and refunded if WhatsApp later reports it failed. Tip: give your WhatsApp template a time-to-live (TTL) no longer than cascade_timeout, so a phone that comes back online does not receive both. |
| cascade_timeout | integer | Optional | With cascade: seconds to wait for a delivery report before the next channel, from 30 to 43200 (12 h). Default: 120. Shorter falls back faster but bills more double sends; longer means fewer duplicates. |
| metadata | object | Optional | Custom key-value pairs stored with the message and forwarded in webhooks (they are not returned by the GET /v1/messages endpoints). One key is meaningful to the platform: purpose: "otp" flags a one-time code you generate yourself and enables automatic redelivery (see notes). |
| buttons | array | Optional | Array of interactive WhatsApp button objects (max 3, type "copy" | "url" | "call"). |
| interactive | object | Optional | Rich WhatsApp interactive message object: dropdown list menus ("list"), quick reply buttons ("button"), native chat forms ("flow"), a link button ("cta_url": action {"name":"cta_url","parameters":{"display_text","url"}}) or a location request ("location_request_message"). |
| template | object | Optional | (WhatsApp) An approved template of your account: {"name", "language"}, values in metadata.variables. The only message type allowed outside the 24-hour window; priced by the template's category. |
| image | video | audio | document | sticker | object | Optional | (WhatsApp) Media by "id" (an uploaded media id) OR by "link" (an https URL), never both. "caption" on image, video and document; "filename" on document. |
| reaction | object | Optional | (WhatsApp) {"message_id": wamid, "emoji": "👍"} — reacts to a message of the conversation; an empty emoji removes the reaction. Not billed. |
| contacts | array | Optional | (WhatsApp) Contact cards (max 20): name.formatted_name required; phones, emails, urls, addresses, org, birthday optional. |
| location | object | Optional | (WhatsApp) Location pin: {"latitude", "longitude", "name"?, "address"?}. |
| context | object | Optional | (WhatsApp) {"message_id": wamid} — replies quoting an earlier message. Valid on every type except reaction. |
| phone_number_id | string | Optional | (WhatsApp) Which connected number sends; defaults to your default number. |
- Sending OTP codes? Two approaches. /v1/verify/send handles the whole cycle (generation, delivery, validation, expiry) and remains the recommended path. If you generate your own codes and send them here, add metadata: {"purpose": "otp"}: on a network-confirmed delivery failure to a Moroccan number while the code is still fresh (under 10 minutes), the platform automatically resends it once over an alternative SMS route — same message id, no extra cost. Without this tag, the message is handled as an ordinary SMS.
- WhatsApp: unless it is a template, a message (text, media, reaction, contacts, location, interactive) only goes to a contact who wrote to your number in the last 24 hours; otherwise the request is refused with 400 OUT_OF_24H_WINDOW before any charge. With a sandbox key the send is still simulated and the response reports the refusal under "warnings". To show "typing…" while you prepare the reply, see POST /v1/messages/typing.
- Line breaks (\n) are fully supported on all channels and display correctly on the recipient side.
- Rich text formatting (bold, italics, etc.) is not supported on the SMS channel (plain text only).
- The WhatsApp channel supports text formatting with the standard syntax (*bold*, _italics_, ~strikethrough~).
curl -X POST "https://api.envoisms.ma/v1/messages" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"message": "Votre code de validation est 849204",
"from": "MaBoutique",
"channel": "whatsapp"
}'{
"id": "msg_8f2d...",
"to": "+212612345678",
"channel": "whatsapp",
"cascade": false,
"status": "queued",
"cost": {
"eur": 0.03,
"mad": 0.33
},
"segments": 1,
"created_at": "2026-05-15T10:30:00Z"
}Bulk send
Sends up to 10,000 messages in a single API call, each with its own recipient or content.
| Field | Type | Requirement | Description |
|---|---|---|---|
| messages | array | Required | Array of objects containing "to", "message" (or "body"), and an optional "metadata" object. |
| from | string | Optional | Global Sender ID for the whole batch. |
| channel | string | Optional | Global channel ("sms" or "whatsapp"). Defaults to "sms". |
- Line breaks (\n) are fully supported in bulk message bodies.
- Rich text formatting (bold, italics, etc.) is not supported on the SMS channel (plain text only).
- The WhatsApp channel supports text formatting with the standard syntax (*bold*, _italics_, ~strikethrough~).
curl -X POST "https://api.envoisms.ma/v1/messages/bulk" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+212611111111",
"message": "Hello Client 1"
},
{
"to": "+212622222222",
"message": "Hello Client 2"
}
],
"from": "ENVOISMS",
"channel": "sms"
}'{
"batch_id": "batch_9a3c...",
"total": 2,
"channel": "sms",
"estimated_cost": {
"eur": 0.056,
"mad": 0.62
},
"messages": [
{
"id": "msg_1a2b...",
"to": "+212611111111",
"status": "queued"
},
{
"id": "msg_3c4d...",
"to": "+212622222222",
"status": "queued"
}
]
}WhatsApp typing indicator
Marks a received WhatsApp message as read and shows its sender "typing…" while you prepare the reply (clears when you reply, or after about 25 seconds). Not a message: nothing is stored or billed.
| Field | Type | Requirement | Description |
|---|---|---|---|
| message_id | string | Required | WhatsApp id (wamid) of the received message you are answering. |
| typing_indicator | boolean | Optional | false sends the read receipt only. Default: true. |
| phone_number_id | string | Optional | Connected number that received the message; defaults to your default number. |
curl -X POST "https://api.envoisms.ma/v1/messages/typing" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"status": "ok",
"message_id": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg",
"typing_indicator": true
}List messages
Retrieves a paginated list of all messages sent from the account.
| Parameter | Type | Requirement | Description |
|---|---|---|---|
| limit | integer | Optional | Number of results to return (1-200, default: 50). |
| offset | integer | Optional | Number of results to skip for pagination (default: 0). |
| status | string | Optional | Filter by status (queued, sent, delivered, failed, undeliverable, unconfirmed). |
| channel | string | Optional | Filter by channel (sms, whatsapp). |
| from_date | string | Optional | Filter by start date (ISO 8601 format). |
| to_date | string | Optional | Filter by end date (ISO 8601 format). |
curl -X GET "https://api.envoisms.ma/v1/messages?limit=50&offset=0" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "msg_8f2d...",
"to": "+212612345678",
"channel": "whatsapp",
"body": "Votre code de validation est 849204",
"sender_id": "MaBanque",
"status": "delivered",
"cost_mad": 0.13,
"created_at": "2026-05-15T10:30:00Z"
}
],
"limit": 50,
"offset": 0,
"total": 1
}Message status
Retrieves the details and real-time delivery status of a specific message.
- Possible statuses: queued, sent, delivered, failed, undeliverable, unconfirmed. "unconfirmed" means the operator never confirmed nor denied delivery — a receipt arriving later can still replace it.
- The metadata field provided at send time is not returned here; it is forwarded in webhooks.
curl -X GET "https://api.envoisms.ma/v1/messages/:id" \
-H "Authorization: Bearer smr_xxx"{
"id": "msg_8f2d...",
"campaign_id": null,
"to": "+212612345678",
"channel": "sms",
"body": "Votre code de validation est 849204",
"sender_id": "MaBanque",
"unicode": 0,
"segments": 1,
"status": "delivered",
"error_code": null,
"error_message": null,
"cost_eur": 0.0436,
"cost_mad": 0.48,
"scheduled_at": null,
"sent_at": "2026-05-15T10:30:02Z",
"delivered_at": "2026-05-15T10:30:05Z",
"failed_at": null,
"operator": "Maroc Telecom",
"sandbox": 0,
"created_at": "2026-05-15T10:30:00Z"
}List templates
Retrieves all approved WhatsApp and SMS templates on your account.
curl -X GET "https://api.envoisms.ma/v1/templates" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "tpl_1234...",
"name": "otp_verification",
"channel": "whatsapp",
"body": "Votre code de validation est {{1}}",
"status": "approved",
"category": "otp",
"created_at": "2026-05-15T10:30:00Z"
}
],
"total": 1
}Create a template
Submits a new template for approval by the carriers or WhatsApp.
| Field | Type | Requirement | Description |
|---|---|---|---|
| name | string | Required | Internal name of the template. |
| channel | string | Required | "sms" or "whatsapp". |
| body | string | Required | Message content with variables (e.g. {{1}}). |
| category | string | Optional | Template category (e.g. "otp", "marketing"). |
| language | string | Optional | WhatsApp: Meta language code (e.g. "fr", "ar", "en_US"). Defaults to "fr". |
| sample_values | object | string[] | Optional | WhatsApp: one example value per variable, required for Meta review (e.g. {"1": "Amine"} or ["Amine"]). |
| header_example_url | string | Optional | WhatsApp, image/video/document header: public https link to an example file, uploaded to Meta for review. |
| add_security_recommendation | boolean | Optional | WhatsApp, "authentication" category: adds Meta's security notice. The body text is fixed by Meta; code_expiration_minutes (1-90) is also accepted. |
- A WhatsApp template first goes through EnvoiSMS review (pending_admin), then is submitted to Meta on your own WhatsApp Business Account (pending_meta). Only Meta approves it (approved); its final category is the one Meta assigns.
curl -X POST "https://api.envoisms.ma/v1/templates" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"id": "tpl_1234...",
"name": "order_confirmation",
"channel": "whatsapp",
"language": "fr",
"status": "pending_admin"
}Sync templates from Meta
Imports every template of your WhatsApp Business Account (status, category, quality). A template Meta no longer holds becomes deleted_on_meta.
curl -X POST "https://api.envoisms.ma/v1/templates/sync" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"ok": true,
"wabas": ["1234567890"],
"fetched": 12,
"updated": 9,
"inserted": 3,
"marked_deleted": 1,
"complete": true,
"errors": []
}List Sender IDs
Retrieves the list of your Sender IDs with their approval status.
curl -X GET "https://api.envoisms.ma/v1/sender-ids" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "sid_9a8b...",
"sender_id": "MABANQUE",
"status": "approved",
"requested_at": "2026-05-10T09:00:00Z"
}
]
}Request a Sender ID
Submits a new Sender ID for approval (required for Morocco).
| Field | Type | Requirement | Description |
|---|---|---|---|
| sender_id | string | Required | The desired sender name (max 11 characters). |
| rc_url | string | Optional | Link to the Trade Register (Registre de Commerce) for verification. |
curl -X POST "https://api.envoisms.ma/v1/sender-ids" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"id": "sid_9a8b...",
"sender_id": "MABANQUE",
"status": "pending"
}WhatsApp Business profile
Retrieves public information of your verified WhatsApp Business profile (name, photo, description, address, website).
curl -X GET "https://api.envoisms.ma/v1/whatsapp/profile" \
-H "Authorization: Bearer smr_xxx"{
"about": "Service client officiel EnvoiSMS.ma",
"address": "Casablanca, Maroc",
"description": "Infrastructure de messagerie programmable pour les entreprises au Maroc.",
"email": "[email protected]",
"websites": [
"https://votremarque.ma"
],
"profile_picture_url": "https://pps.whatsapp.net/v/..."
}Update WhatsApp profile
Updates the customer-facing information on your official WhatsApp Business profile.
| Field | Type | Requirement | Description |
|---|---|---|---|
| about | string | Optional | Short text status (max 139 characters). |
| description | string | Optional | Detailed business description (max 512 characters). |
| address | string | Optional | Physical address of your office or store. |
| string | Optional | Customer support contact email address. | |
| websites | array | Optional | Array containing up to 2 website URLs. |
curl -X POST "https://api.envoisms.ma/v1/whatsapp/profile" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"success": true,
"updated_at": "2026-08-30T10:00:00Z"
}Generate an OTP
Sends a one-time verification code. Two modes, your choice: "whatsapp" — managed verification, the simplest (EnvoiSMS generates the code and delivers it on WhatsApp from a verified sender; if Meta refuses the WhatsApp send, the code goes out by SMS; with a Verify app configured for auto_cascade, the SMS also goes out once the channel timeout expires, 30 s by default; no code to store on your side) — or "sms" — you keep full control of the code, the template and the sender. In both cases, validation is done with the same /v1/verify/check call. Want your own brand on the managed verification's SMS fallback? Available on request: [email protected].
| Field | Type | Requirement | Description |
|---|---|---|---|
| to | string | Required | Recipient in E.164 format. |
| channel | string | Optional | "whatsapp" (managed verification: WhatsApp, SMS fallback if Meta refuses the send or, with auto_cascade, after the channel timeout; code valid 10 min, 3 attempts, billed per verification at the WhatsApp OTP rate) or "sms" (code generated for you, sent with your brand). Default: "sms". |
| app_id | string | Optional | ID of the Verify application configured on the dashboard (e.g. vra_...). Automatically applies the code settings, timings and channel cascade. |
| brand | string | Optional | (sms channel) Displayed brand name (e.g. MonApp, max 32 chars). Defaults to "EnvoiSMS". |
| code_length | integer | Optional | (sms channel) Length of the generated code (4 to 8 digits, default: 6). |
| expiry | integer | Optional | Code validity duration in seconds (60 to 1800, default: 600). On the whatsapp channel it is capped at 600 (the authentication template's limit). |
| cascade | array | Optional | (sms channel) Ordered list of channels for automatic cascading fallback (e.g. ["whatsapp", "sms"]). |
| template | string | Optional | (sms channel) Custom text with the {{code}} and {{brand}} variables. In whatsapp mode, the localized message (fr/en/es) is managed for you. |
| otp_button_text | string | Optional | (whatsapp channel) Custom label for the WhatsApp auto-copy button (max 25 chars). |
| web_otp_domain | string | Optional | (Optional, sms channel) Target web domain for W3C WebOTP autofill (e.g. "https://mysite.ma"). Appends @domain #code to the SMS. |
| app_hash | string | Optional | (Optional, sms channel) 11-character Android app signature hash (SMS Retriever API) for automatic OTP detection on Android. |
- OTP Autofill (iOS & Android): To allow your users to automatically 1-tap fill the received code above their keyboard, simply add autocomplete="one-time-code" and inputmode="numeric" to the <input> field on your website.
- Anti-spam cooldown: The "resend_after_seconds" field indicates the exact cooldown before retrying (60s for WhatsApp according to Meta standards, 30s for SMS). A daily cap of 10 verifications per destination applies.
curl -X POST "https://api.envoisms.ma/v1/verify/send" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"brand": "MonApp",
"channel": "whatsapp",
"code_length": 6,
"expiry": 600
}'{
"session_id": "vrf_7e2a...",
"to": "+212612345678",
"channel": "whatsapp",
"resend_after_seconds": 60,
"fallback_channels": ["sms", "whatsapp"],
"expires_at": "2026-05-15T10:35:00Z",
"status": "sent",
"cost": { "eur": 0.05, "mad": 0.55 }
}Resend OTP Code (SMS or WhatsApp)
Resends the same active OTP code over SMS or WhatsApp after the cooldown (30s for SMS, 60s for WhatsApp). No new code is generated, preventing user input conflicts.
| Field | Type | Requirement | Description |
|---|---|---|---|
| session_id | string | Required | Session ID received from the initial /v1/verify/send call. |
| channel | string | Optional | Target resend channel ("sms" | "whatsapp"). Default: "sms". |
- Available on every plan, Starter included: /v1/verify/resend requires the same "verify" permission as /v1/verify/send, nothing more.
- Where the countdown starts: the cooldown runs from the last actual delivery, not from your call — 60s after a WhatsApp delivery, 30s after an SMS one. GET /v1/verify/{session_id} returns the live countdown ("resend_available_in_seconds") and the 429's Retry-After header carries the exact seconds remaining. The window is scoped to your account and the destination: another customer's traffic to the same number never blocks you.
- Sandbox keys (env_test_): nothing is delivered and nothing is charged. The response then carries "sandbox": true and "sandbox_code", so a simulated resend is never mistaken for a delivered one. A sandbox key can only resend sessions it created, and a live key only live sessions.
curl -X POST "https://api.envoisms.ma/v1/verify/resend" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"session_id": "vrf_7e2a...",
"message_id": "wamid.HBgMMjEy...",
"to": "+212612345678",
"channel": "whatsapp",
"status": "sent",
"resend_after_seconds": 60,
"expires_at": "2026-05-15T10:35:00Z"
}Verify an OTP
Validates the code provided by the user for a given verification session.
| Field | Type | Requirement | Description |
|---|---|---|---|
| session_id | string | Required | Session ID received from the /v1/verify/send call. |
| code | string | Required | The code received and entered by the user. |
curl -X POST "https://api.envoisms.ma/v1/verify/check" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"session_id": "vrf_7e2a...",
"code": "849204"
}'{
"session_id": "vrf_7e2a...",
"verified": true,
"verified_at": "2026-05-15T10:35:12Z"
}OTP session status
Retrieves the state (validated or expired) of a specific verification session.
curl -X GET "https://api.envoisms.ma/v1/verify/:id" \
-H "Authorization: Bearer smr_xxx"{
"id": "vrf_7e2a...",
"to": "+212612345678",
"channel": "whatsapp",
"expires_at": "2026-05-15T10:40:00Z",
"verified_at": "2026-05-15T10:35:12Z",
"created_at": "2026-05-15T10:30:00Z"
}Number lookup
Validates the format, carrier, line type and geographic location of a phone number.
| Field | Type | Requirement | Description |
|---|---|---|---|
| number | string | Required | The phone number to validate (local or international format). |
| country_code | string | Optional | 2-letter ISO country code (e.g. MA, FR). Recommended when the number is in local format. |
curl -X POST "https://api.envoisms.ma/v1/verify/lookup" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"number": "+212612345678",
"country_code": "MA"
}'{
"valid": true,
"number": "212612345678",
"local_format": "0612345678",
"international_format": "+212612345678",
"country_prefix": "212",
"country_code": "MA",
"country_name": "Morocco",
"location": "Casablanca",
"carrier": "Maroc Telecom (IAM)",
"line_type": "mobile"
}List conversations
Retrieves active WhatsApp conversation threads with real-time 24-hour service window tracking and bot mute status.
| Parameter | Type | Requirement | Description |
|---|---|---|---|
| limit | integer | Optional | Maximum number of conversations to return (default 30, max 100). |
| bot_status | string | Optional | Filter by bot status ("active" or "muted"). |
curl -X GET "https://api.envoisms.ma/v1/conversations?limit=50&offset=0" \
-H "Authorization: Bearer smr_xxx"{
"conversations": [
{
"phone": "+212612345678",
"contact_name": "Yassine Alami",
"last_message": "Bonjour, je souhaite visiter l'appartement témoin",
"last_message_at": "2026-08-30T11:42:00Z",
"unread_count": 1,
"bot_muted": true,
"can_reply_free": true,
"window_expires_at": "2026-08-31T11:42:00Z"
}
],
"total": 1
}Conversation history
Retrieves the complete message thread exchanged with a specific WhatsApp contact.
curl -X GET "https://api.envoisms.ma/v1/conversations/:phone" \
-H "Authorization: Bearer smr_xxx"{
"phone": "+212612345678",
"contact_name": "Yassine Alami",
"bot_muted": true,
"messages": [
{
"id": "msg_01J...",
"direction": "inbound",
"body": "Je souhaite des informations sur le projet Casablanca Marina",
"type": "text",
"timestamp": "2026-08-30T11:40:00Z"
},
{
"id": "msg_02J...",
"direction": "outbound",
"body": "Bonjour Yassine ! Quel type de bien recherchez-vous ?",
"type": "interactive",
"timestamp": "2026-08-30T11:40:05Z"
}
]
}Send live chat reply
Sends an agent reply and mutes the bot on that conversation (re-enable with POST /v1/conversations/:phone/toggle-bot). A free-form WhatsApp message requires the contact to have written in the last 24 hours (otherwise 400 OUT_OF_24H_WINDOW); an approved template can be sent at any time.
| Field | Type | Requirement | Description |
|---|---|---|---|
| message | string | Optional | Text of the reply (or caption of the attached media). Required without a template or media. |
| channel | string | Optional | "whatsapp" (default) or "sms". |
| template_name | string | Optional | Approved WhatsApp template to send instead of free text (with template_language and variables). |
| media_url | string | Optional | https URL (or data URI) of an image or PDF to attach; media_type "image" or "document". |
| phone_number_id | string | Optional | Connected WhatsApp number that replies. |
curl -X POST "https://api.envoisms.ma/v1/conversations/:phone/messages" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"ok": true,
"message": {
"id": "msg_03J...",
"phone": "212612345678",
"role": "agent",
"message": "Bonjour Yassine, votre visite est confirmée.",
"status": "sent",
"channel": "whatsapp",
"created_at": "2026-08-30T11:45:00Z"
}
}List qualified leads
Retrieves leads automatically qualified by AtlasAI™ with their BANT score and CRM pipeline stage.
| Parameter | Type | Requirement | Description |
|---|---|---|---|
| stage | string | Optional | Filter by pipeline stage ("new", "contacted", "meeting_scheduled", "closed_won", "closed_lost"). |
| min_score | integer | Optional | Minimum BANT score (0 to 100). |
- Leads are scored by AtlasAI™ Conversational Engine, AtlasAI™ Voice, and AtlasAI™ Vision directly from inbound WhatsApp messages. These AI modules are integrated natively within WABA and are not available as standalone external APIs.
curl -X GET "https://api.envoisms.ma/v1/qualified-leads?limit=50&offset=0" \
-H "Authorization: Bearer smr_xxx"{
"leads": [
{
"id": "lead_01J...",
"phone": "+212612345678",
"name": "Dr. Benjelloun",
"preset": "medical_equipment",
"bant_score": 85,
"is_hot": true,
"breakdown": {
"budget": 25,
"authority": 25,
"need": 20,
"timeline": 15
},
"intent": "Échographe Doppler pour nouveau cabinet",
"stage": "meeting_scheduled",
"created_at": "2026-08-30T09:15:00Z"
}
],
"total": 1
}Update lead status
Updates the sales pipeline stage for a qualified lead.
| Field | Type | Requirement | Description |
|---|---|---|---|
| status | string | Required | "new" | "contacted" | "meeting_scheduled" | "closed_won" | "closed_lost" |
| notes | string | Optional | Internal sales follow-up notes. |
curl -X POST "https://api.envoisms.ma/v1/qualified-leads/:id/status" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"success": true,
"lead_id": "lead_01J...",
"stage": "meeting_scheduled"
}List contacts
Retrieves all contacts on your account, with keyword or list filtering.
| Parameter | Type | Requirement | Description |
|---|---|---|---|
| limit | integer | Optional | Results per page (1-500, default: 100). |
| offset | integer | Optional | Pagination offset (default: 0). |
| q | string | Optional | Search by name, phone or email. |
| list_id | string | Optional | Filter to members of a specific contact list only. |
curl -X GET "https://api.envoisms.ma/v1/contacts?limit=50&offset=0" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "ctc_a2b3...",
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "[email protected]",
"custom1": "VIP",
"custom2": null,
"custom3": null,
"created_at": "2026-05-10T14:20:00Z"
}
],
"limit": 100,
"offset": 0,
"total": 1
}Create / Update a contact
Adds a contact or updates an existing contact's information (matched by number).
| Field | Type | Requirement | Description |
|---|---|---|---|
| phone | string | Required | Phone number in E.164 format. |
| name | string | Optional | Full name of the contact. |
| string | Optional | Email address. | |
| list_id | string | Optional | Immediately attach the contact to an existing list. |
| custom_fields | object | Optional | Object containing up to 3 custom fields ("custom1", "custom2", "custom3"). |
curl -X POST "https://api.envoisms.ma/v1/contacts" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "[email protected]",
"list_id": "lst_f84b..."
}'{
"id": "ctc_a2b3...",
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "[email protected]",
"custom1": "VIP",
"custom2": null,
"custom3": null,
"created_at": "2026-05-10T14:20:00Z"
}Import contacts
Bulk-imports up to 5,000 contacts in a single call.
| Field | Type | Requirement | Description |
|---|---|---|---|
| contacts | array | Required | Array of objects containing "phone", "name" (optional) and "email" (optional). |
| list_id | string | Optional | ID of the list to import the group into. |
curl -X POST "https://api.envoisms.ma/v1/contacts/import" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{
"phone": "+212611111111",
"name": "Karim"
},
{
"phone": "+212622222222",
"name": "Youssef"
}
],
"list_id": "lst_f84b..."
}'{
"imported": 150,
"skipped": 3
}Delete a contact
Permanently deletes a contact by its unique identifier.
curl -X DELETE "https://api.envoisms.ma/v1/contacts/:id" \
-H "Authorization: Bearer smr_xxx"{
"deleted": true,
"id": "ctc_a2b3..."
}List contact lists
Retrieves all contact lists created for broadcast campaigns.
curl -X GET "https://api.envoisms.ma/v1/contacts/lists" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "lst_f84b...",
"name": "Newsletter Clients",
"description": "Clients inscrits à notre lettre d'information",
"count": 1420,
"created_at": "2026-04-15T09:00:00Z"
}
]
}Create a list
Creates a new empty group (contact list) intended for campaigns.
| Field | Type | Requirement | Description |
|---|---|---|---|
| name | string | Required | Name of the list. |
| description | string | Optional | Description of the list. |
curl -X POST "https://api.envoisms.ma/v1/contacts/lists" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Newsletter Clients",
"description": "Clients inscrits"
}'{
"id": "lst_f84b...",
"name": "Newsletter Clients",
"description": "Clients inscrits",
"count": 0
}List opt-outs
Retrieves the list of numbers that opted out (STOP) of your communications.
curl -X GET "https://api.envoisms.ma/v1/optouts" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"phone": "+212611111111",
"opted_out_at": "2026-06-01T12:00:00Z"
}
],
"total": 1
}Add an opt-out
Manually adds a number to your opt-out list (global blacklist).
| Field | Type | Requirement | Description |
|---|---|---|---|
| phone | string | Required | Phone number in E.164 format. |
curl -X POST "https://api.envoisms.ma/v1/optouts" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"success": true,
"phone": "+212611111111",
"opted_out_at": "2026-06-01T12:00:00Z"
}Remove an opt-out
Removes a number from the opt-out list.
curl -X DELETE "https://api.envoisms.ma/v1/optouts/:phone" \
-H "Authorization: Bearer smr_xxx"{
"deleted": true,
"phone": "+212611111111"
}List consent events
Append-only consent ledger (Law 09-08, art. 10): every grant and every withdrawal is a timestamped event with its source and evidence. The platform writes STOP/START keywords received on WhatsApp and SMS and the first customer-initiated conversation itself. Add format=csv to export the whole filtered set.
| Parameter | Type | Requirement | Description |
|---|---|---|---|
| phone | string | Optional | E.164 number to filter on. |
| purpose | string | Optional | marketing, transactional, otp or service. |
| status | string | Optional | granted or withdrawn. |
| from | string | Optional | Events recorded at or after this ISO 8601 timestamp. |
| format | string | Optional | csv to download the full set as a file. |
curl -X GET "https://api.envoisms.ma/v1/consents?limit=50&offset=0" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "cns_9f2a...",
"phone": "+212612345678",
"channel": "whatsapp",
"purpose": "marketing",
"status": "withdrawn",
"source": "whatsapp_keyword",
"evidence": { "keyword": "stop", "lang": "fr" },
"recorded_at": "2026-09-05T09:00:00.000Z",
"created_at": "2026-09-05T09:00:00.000Z"
}
],
"total": 1,
"limit": 100,
"offset": 0
}Record a consent event
Records that a person granted or withdrew consent, with the evidence you hold (wording shown, URL, IP, reference). recorded_at may be backdated for an imported consent, never set in the future. A marketing withdrawal is placed on the opt-out list immediately.
| Field | Type | Requirement | Description |
|---|---|---|---|
| phone | string | Required | Phone number in E.164 format. |
| purpose | string | Required | marketing, transactional, otp or service. |
| status | string | Optional | granted (default) or withdrawn. |
| channel | string | Optional | whatsapp, sms or any (default). |
| source | string | Optional | api (default), form, import or dashboard. Keyword sources are reserved to the platform. |
| evidence | object | Optional | Free-form JSON object (< 4 KB): wording shown, url, ip, reference. |
| recorded_at | string | Optional | ISO 8601 timestamp of the person's act (default: now). |
curl -X POST "https://api.envoisms.ma/v1/consents" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"id": "cns_9f2a...",
"phone": "+212612345678",
"channel": "any",
"purpose": "marketing",
"status": "granted",
"source": "form",
"evidence": { "text": "J'accepte de recevoir les offres par WhatsApp", "url": "https://example.ma/inscription" },
"recorded_at": "2026-09-01T10:00:00.000Z",
"created_at": "2026-09-09T12:00:00.000Z"
}Consent state and history for a number
The answer to “show me this person's consent”: current state per purpose (derived from the newest event — unknown without history, never assumed granted), opt-out list membership, and the full history.
curl -X GET "https://api.envoisms.ma/v1/consents/:phone" \
-H "Authorization: Bearer smr_xxx"{
"phone": "+212612345678",
"opted_out": true,
"opted_out_at": "2026-09-05T09:00:00Z",
"state": {
"marketing": { "status": "withdrawn", "channel": "whatsapp", "source": "whatsapp_keyword", "recorded_at": "2026-09-05T09:00:00.000Z", "record_id": "cns_3" },
"transactional": { "status": "unknown" },
"otp": { "status": "unknown" },
"service": { "status": "granted", "channel": "whatsapp", "source": "whatsapp_inbound", "recorded_at": "2026-07-01T09:00:00.000Z", "record_id": "cns_1" }
},
"history": []
}List campaigns
Retrieves all your scheduled or currently running broadcast campaigns.
curl -X GET "https://api.envoisms.ma/v1/campaigns" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "cmp_8d2a...",
"name": "Soldes d'été 2026",
"channel": "sms",
"list_id": "lst_f84b...",
"template_id": null,
"body": "Bonjour {{name}}, profitez de -50% sur toute la collection avec le code ETE50 !",
"sender_id": "SOLDES",
"status": "draft",
"scheduled_at": null,
"created_at": "2026-06-01T12:00:00Z"
}
]
}Create a campaign
Saves a new campaign as a draft or schedules it for a specific date.
| Field | Type | Requirement | Description |
|---|---|---|---|
| name | string | Required | Name of the campaign. |
| body | string | Required | Message body. May use the {{name}} variable. (One of the two is required: body or template_id.) |
| template_id | string | Required | Alternatively, the ID of an approved template. (One of the two is required: body or template_id.) |
| channel | string | Optional | "sms" or "whatsapp". Defaults to "sms". |
| list_id | string | Optional | ID of the recipient contact list. |
| sender_id | string | Optional | Sender name. |
| scheduled_at | string | Optional | Scheduled date (ISO 8601). Sets the status to "scheduled". |
| buttons | array | Optional | Array of interactive WhatsApp buttons (max 3, type "copy" | "url" | "call"). |
| metadata | object | Optional | Custom metadata (e.g. throttling / send-rate options: {"throttling": "50_min"}). |
curl -X POST "https://api.envoisms.ma/v1/campaigns" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Soldes d'été 2026",
"body": "Bonjour {{name}}, profitez de -50% avec le code ETE50 !",
"channel": "sms",
"list_id": "lst_f84b...",
"sender_id": "SOLDES"
}'{
"id": "cmp_8d2a...",
"status": "draft"
}Campaign details
Retrieves the details, schedule and execution status of a campaign.
curl -X GET "https://api.envoisms.ma/v1/campaigns/:id" \
-H "Authorization: Bearer smr_xxx"{
"id": "cmp_8d2a...",
"name": "Soldes d'été 2026",
"channel": "sms",
"list_id": "lst_f84b...",
"body": "Bonjour {{name}}, profitez de -50%...",
"sender_id": "SOLDES",
"status": "running",
"total_count": 1420,
"sent_count": 840,
"started_at": "2026-06-15T10:00:00Z",
"created_at": "2026-06-01T12:00:00Z"
}Launch a campaign
Immediately starts broadcasting a draft campaign to all associated contacts.
curl -X POST "https://api.envoisms.ma/v1/campaigns/:id/send" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"id": "cmp_8d2a...",
"queued": 1420,
"total": 1420
}List webhooks
Retrieves the list of all your registered webhook endpoints.
curl -X GET "https://api.envoisms.ma/v1/webhooks" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "whk_5f2b...",
"url": "https://mon-serveur.ma/api/envoisms-receiver",
"events": [
"message.delivered",
"message.failed"
],
"active": true,
"last_triggered_at": "2026-06-15T09:30:15Z",
"last_status": 200,
"created_at": "2026-05-01T10:00:00Z"
}
]
}Create a Webhook
Registers an HTTPS callback URL to receive event notifications.
| Field | Type | Requirement | Description |
|---|---|---|---|
| url | string | Required | Secure target URL starting with "https://". |
| events | array | Optional | Array of events (e.g. ["message.delivered", "message.failed"]). The wildcards "message.*" and "*" are accepted. Default: ["message.delivered", "message.failed"]. |
| secret | string | Optional | Secret signing key. If not provided, it will be generated automatically. |
curl -X POST "https://api.envoisms.ma/v1/webhooks" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mon-serveur.ma/api/envoisms-receiver",
"events": [
"message.delivered",
"message.failed"
]
}'{
"id": "whk_5f2b...",
"url": "https://mon-serveur.ma/api/envoisms-receiver",
"events": [
"message.delivered",
"message.failed"
],
"secret": "whsec_2f8a9e7d...",
"active": true
}Update a Webhook
Updates a webhook's configuration (URL, subscribed events or active state).
| Field | Type | Requirement | Description |
|---|---|---|---|
| url | string | Optional | New HTTPS URL. |
| events | array | Optional | New list of subscribed events. |
| active | boolean | Optional | Enable (true) or disable (false) the webhook. |
curl -X PATCH "https://api.envoisms.ma/v1/webhooks/:id" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://mon-serveur.ma/api/envoisms-receiver-updated",
"active": false
}'{
"updated": true,
"id": "whk_5f2b..."
}Delete a Webhook
Deactivates and soft-deletes a webhook endpoint.
curl -X DELETE "https://api.envoisms.ma/v1/webhooks/:id" \
-H "Authorization: Bearer smr_xxx"{
"deleted": true,
"id": "whk_5f2b..."
}Test a Webhook
Triggers a test event ("message.test") to the webhook's configured URL.
curl -X POST "https://api.envoisms.ma/v1/webhooks/:id/test" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"queued": true,
"id": "whk_5f2b..."
}List Inbound Webhooks
Retrieves all configured advertising lead capture endpoints on your account.
curl -X GET "https://api.envoisms.ma/v1/inbound-webhooks" \
-H "Authorization: Bearer smr_xxx"{
"webhooks": [
{
"id": "inw_9a8b...",
"name": "Campagne TikTok Ads Casablanca",
"source": "tiktok_ads",
"catch_url": "https://api.envoisms.ma/v1/inbound-webhooks/catch/inw_9a8b...",
"auto_start_funnel": true,
"total_received": 142,
"created_at": "2026-08-20T14:00:00Z"
}
]
}Create an Inbound Webhook
Generates a catch URL to receive instant leads from TikTok Lead Ads, Meta Lead Ads, Zapier, or Make.
| Field | Type | Requirement | Description |
|---|---|---|---|
| name | string | Required | Descriptive source name (e.g. "Facebook Lead Gen Summer Promo"). |
| source | string | Optional | Source identifier ("tiktok_ads", "meta_leads", "google_forms", "custom"). Defaults to "custom". |
| auto_start_funnel | boolean | Optional | If true, immediately initiates the AtlasAI™ WhatsApp qualification funnel upon lead receipt. |
curl -X POST "https://api.envoisms.ma/v1/inbound-webhooks" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"id": "inw_9a8b...",
"name": "Campagne TikTok Ads Casablanca",
"catch_url": "https://api.envoisms.ma/v1/inbound-webhooks/catch/inw_9a8b...",
"auto_start_funnel": true
}Check balance
Retrieves the available balance in Moroccan Dirhams (MAD) along with the currency and active plan.
curl -X GET "https://api.envoisms.ma/v1/billing/balance" \
-H "Authorization: Bearer smr_xxx"{
"balance_mad": 1492.5,
"currency": "MAD",
"plan": "croissance"
}Machine payments status
Reports whether machine payments (MPP / HTTP 402) are active on this instance. An agent can check this before attempting a paid call.
- When "enabled" is false, any paid call returns 503 instead of 402.
curl -X GET "https://api.envoisms.ma/v1/machine-payments/status" \
-H "Authorization: Bearer smr_xxx"{
"enabled": true,
"deposit_usd": 5.00,
"credit_mad": 49.65,
"networks": ["base", "tempo"],
"permissions": ["send", "status", "balance", "verify"]
}Payment challenge (HTTP 402)
Any paid endpoint called without an API key returns a 402 with a WWW-Authenticate: Payment header carrying a signed challenge and a USDC deposit address. The agent deposits exactly deposit_usd USDC to one of the addresses, then retries the call with a Payment credential.
- The "request" field is canonical JSON (RFC 8785) base64url-encoded without padding. It contains: amount, currency, networks[], recipients{}, paymentIntent, resource, credit{}.
- IP deduplication: if an active challenge exists for the same IP, the same challenge is returned rather than a new deposit address.
- Rate limit: 5 challenges per minute per IP. Beyond that, the 402 is returned without a deposit address.
curl -X POST "https://api.envoisms.ma/v1/messages" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"message": "Votre code de validation est 849204",
"from": "MaBoutique",
"channel": "whatsapp"
}'HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment id="chal_01jxyz", realm="envoisms",
method="stripe", intent="session",
request="<base64url-JCS-encoded JSON>",
expires="2026-09-25T21:45:00.000Z",
description="EnvoiSMS pay-per-use API access."
{
"type": "https://paymentauth.org/problems/payment-required",
"title": "Payment Required",
"status": 402,
"detail": "Payment is required.",
"challengeId": "chal_01jxyz"
}Redeem a payment credential
After depositing USDC funds, the agent retries the original paid endpoint (e.g. POST /v1/messages) replacing Authorization: Bearer with Authorization: Payment <credential>. The worker validates the Stripe deposit server-side, credits a machine account, and returns the scoped API key in the X-Machine-Session-Key response header.
- The credential is canonical JSON base64url: {"challenge":{"id":"chal_01jxyz","method":"stripe","intent":"session"},"source":"<wallet-address>","payload":{}}.
- The returned key is shown once only. Idempotent: an agent that loses its key can call again — the same deposit returns a new key to the same account.
- Returns 402 if the Stripe deposit has not yet reached "succeeded" status. The agent should wait for on-chain confirmation (typically 1–2 minutes) before retrying.
curl -X POST "https://api.envoisms.ma/v1/messages (+ /v1/verify, /v1/waba)" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{}'{
"key": "smr_live_xxxxxxxxxxxxxxxxxxxx",
"permissions": ["send", "status", "balance", "verify"],
"credit_mad": 49.65,
"receipt": "<base64url-encoded receipt>",
"account": {
"id": "acct_01jxyz",
"plan": "starter",
"balance_mad": 49.65
}
}Usage statistics
Retrieves key metrics about your sends (total volumes, delivery rate, and billed costs).
curl -X GET "https://api.envoisms.ma/v1/analytics" \
-H "Authorization: Bearer smr_xxx"{
"summary": {
"total": 12840,
"delivered": 12570,
"delivery_rate": 97.9,
"cost_mad": 2663.1
}
}List API keys
Retrieves the list of all your active or revoked API keys.
curl -X GET "https://api.envoisms.ma/v1/api-keys" \
-H "Authorization: Bearer smr_xxx"{
"data": [
{
"id": "key_e84c...",
"name": "Production Server",
"key_prefix": "smr_a8f7d6c5",
"sandbox": false,
"ip_whitelist": [
"196.200.1.4"
],
"rate_limit": 100,
"permissions": [
"send",
"verify",
"status"
],
"active": true,
"last_used_at": "2026-06-15T10:30:00Z",
"created_at": "2026-05-01T08:00:00Z"
}
]
}Create an API key
Generates a new secure API token with specific permissions and restrictions.
| Field | Type | Requirement | Description |
|---|---|---|---|
| name | string | Optional | Label to identify the key (default: "API key"). |
| permissions | array | Optional | Granted rights (send, verify, status, balance, contacts, campaigns, webhooks, billing, analytics, api_keys, optouts, sender_id). |
| ip_whitelist | array | Optional | List of IP addresses allowed to make requests with this key. |
| rate_limit | integer | Optional | Maximum requests/min limit (10 to 2000). Default by plan: Starter 60, Business 180, Pro 500, Enterprise 1,200. |
| sandbox | boolean | Optional | true generates a test key prefixed env_test_. Requests are validated and recorded, but no message is actually sent and nothing is billed. A key's mode is permanent: to change it, create a new key. |
curl -X POST "https://api.envoisms.ma/v1/api-keys" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Production Server",
"permissions": [
"send",
"verify",
"status"
],
"ip_whitelist": [
"196.200.1.4"
],
"rate_limit": 100
}'{
"id": "key_e84c...",
"name": "Production Server",
"key_prefix": "smr_a8f7d6c5",
"api_key": "smr_a8f7d6c5b4a3...",
"sandbox": false,
"permissions": [
"send",
"verify",
"status"
],
"rate_limit": 100,
"warning": "The full API key is shown once. Store it securely."
}Update an API key
Updates the permissions, IP restrictions or activation state of an API key.
| Field | Type | Requirement | Description |
|---|---|---|---|
| name | string | Optional | New name. |
| permissions | array | Optional | New list of permissions. |
| ip_whitelist | array | Optional | New list of allowed IP addresses. |
| rate_limit | integer | Optional | New per-minute rate limit. |
| active | boolean | Optional | Enable or suspend the key. |
curl -X PATCH "https://api.envoisms.ma/v1/api-keys/:id" \
-H "Authorization: Bearer smr_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Backup Server",
"active": true
}'{
"updated": true,
"id": "key_e84c..."
}Revoke an API key
Permanently revokes an API key to prevent it from authenticating requests.
curl -X DELETE "https://api.envoisms.ma/v1/api-keys/:id" \
-H "Authorization: Bearer smr_xxx"{
"revoked": true,
"id": "key_e84c..."
}Webhooks Guide
Signed and secure delivery callbacks.
EnvoiSMS.ma posts JSON events to your server's HTTPS URL with an X-EnvoiSMS.ma-Signature header to authenticate the sender.
message.sentThe message was accepted by the operator's network.
message.deliveredThe message was successfully delivered to the recipient (SMS or WhatsApp).
message.readThe WhatsApp message was opened and read by the recipient (blue checkmarks).
message.failedDelivery failed at the routing or submission stage (rejection, routing error).
message.undeliverableThe network confirmed the message cannot be delivered (non-existent number, expired in the operator queue).
message.fallbackCascade (cascade: true): the message moves to the next channel (e.g. WhatsApp → SMS) because the first refused or reported the message failed, or sent no delivery report within cascade_timeout. channel is the new channel, previous_channel the old one, error_code and error_message the reason.
message.inboundA recipient replied to one of your SMS or WhatsApp messages (inbound).
message.flow_responseA user completed and submitted a native WhatsApp Flow in-chat form.
message.locationA user shared their GPS location pin on WhatsApp.
lead.qualifiedAtlasAI™ completed the BANT qualification of a lead on WhatsApp.
contact.optoutA recipient unsubscribed (STOP keyword or equivalent).
import crypto from 'node:crypto';
// La signature arrive dans le header X-EnvoiSMS-Signature
// (format "sha256=<hex>"), l'événement dans X-EnvoiSMS-Event.
// Corps livré : { "event": "...", "data": { ... }, "timestamp": "..." }
export function verifySignature(body: string, sig: string, secret: string) {
const hmac = crypto.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from('sha256=' + hmac));
} OpenAPI Spec Errors
API error response structure.
All API errors return an appropriate HTTP code (4xx or 5xx) along with a predictable JSON envelope containing the error code and description.
{
"error": {
"code": "INVALID_PHONE",
"message": "to must be E.164 format, for example +212612345678",
"docs": "https://envoisms.ma/docs#errors"
}
}UNAUTHORIZEDMissing or invalid API key.
INSUFFICIENT_BALANCEInsufficient balance to perform the send.
INVALID_PHONEThe phone number is not in E.164 format.
WHATSAPP_NOT_CONNECTEDNo WhatsApp Business number connected: connect yours in the dashboard to send on WhatsApp. For an ordinary text, omit channel (SMS is the default) — nothing was charged.
OUT_OF_24H_WINDOWFree-form WhatsApp message to a contact who has not written to you in the last 24 hours. Send an approved template. Nothing was charged.
WHATSAPP_ONLY_FIELDA WhatsApp-only field (reaction, contacts, location, sticker, context) was sent on another channel.
CONFLICTING_MESSAGE_TYPESA message carries one content type: reaction, contacts, location or sticker combine with nothing else.
CASCADE_NOT_SUPPORTEDcascade is not possible with a reaction, contacts, a location or a sticker: there is no SMS equivalent.
INVALID_REACTIONreaction needs the target message_id (wamid) and a single emoji (or an empty string to remove the reaction).
INVALID_CONTEXTcontext needs the message_id (wamid) of the quoted message, and cannot be combined with reaction.
INVALID_CONTACTScontacts must be an array of 1 to 20 cards, each with name.formatted_name; the message names the faulty card.
INVALID_LOCATIONlocation requires numeric latitude (-90 to 90) and longitude (-180 to 180); name and address are optional.
INVALID_MEDIAA WhatsApp media object takes either "id" or "link" (an https URL), never both; no caption on audio and sticker.
META_WINDOW_EXPIREDWhatsApp failure (131047): the 24-hour service window was closed. Send an approved template or an SMS.
META_UNDELIVERABLEWhatsApp failure (131026): the number is not reachable on WhatsApp. Not retried automatically.
META_MARKETING_LIMITWhatsApp failure (131049): per-recipient marketing message limit. Do not resend right away.
META_RATE_LIMITEDWhatsApp failure (130429 number throughput, or 131056 as META_PAIR_RATE_LIMITED: too many messages to this contact) after 3 automatic attempts.
META_POLICY_BLOCKEDWhatsApp failure (368; also META_ACCOUNT_LOCKED 131031, META_PAYMENT_ISSUE 131042, META_PHONE_NOT_REGISTERED 133010): sending number or account restricted. Check WhatsApp Manager.
META_TEMPLATE_NOT_FOUNDWhatsApp failure (132001; also META_TEMPLATE_PARAM_MISMATCH 132000, META_TEMPLATE_PARAM_FORMAT 132012): template missing in that language, or wrong variables.
RATE_LIMITEDPer-minute request limit exceeded. Honor the Retry-After header.
INVALID_IDEMPOTENCY_KEYThe Idempotency-Key header exceeds 255 characters.
IDEMPOTENCY_IN_FLIGHTThe original request carrying this Idempotency-Key is still in flight. Retry in a moment.
IDEMPOTENCY_KEY_REUSEDThis Idempotency-Key was already used with a different request body. Use a new key.
INVALID_CHANNELThe requested channel does not exist. Valid channels: sms, whatsapp, telegram, voice, rcs.
CHANNEL_NOT_CONFIGUREDThe requested channel is not currently available on the platform.
CHANNEL_DISABLEDThe requested channel is disabled on the platform.
FORBIDDENThe API key lacks the permission required for this action, or the account is suspended.
SENDER_ID_TOO_LONGThe Sender ID exceeds the 11-character limit.
SENDER_ID_INVALIDThe Sender ID contains disallowed characters.
SENDER_ID_NOT_APPROVEDThe Sender ID has not yet been approved by the carriers.
SENDER_ID_PENDINGThe Sender ID is pending approval.
SENDER_ID_REJECTEDThe Sender ID was rejected by the carriers.
SENDER_ID_GENERICThe Sender ID is a generic header (INFO, ALERT, SERVICE…), not a brand. Use your brand name or omit sender_id.
SENDER_ID_PROTECTED_BRANDThe Sender ID names or resembles a protected institution (bank, carrier, public body). Reserved to accounts whose registration for that name was approved.
SENDER_ID_NOT_REGISTEREDThe Sender ID was never registered on this account and the account has no completed top-up yet: sending is limited to your own verified number.
TRIAL_RESTRICTED_DESTINATIONFree trial account: test sends are restricted to your own verified phone number. Complete a first top-up to send to other recipients.
MISSING_FIELDA required field is missing from the request.
CASCADE_TIMEOUTThe first channel timed out; switching to the secondary channel (cascade).
UPSTREAM_ERRORDelivery error at the operator or gateway level.
OPTED_OUTThe number opted out of your communications (STOP). Sending is forbidden.
SPAM_OR_PHISHING_DETECTEDThe message contains a link identified as phishing, or speaks as a bank, carrier or public body. The send was refused and the account suspended pending review.
CONTENT_BLOCKEDThe message content was blocked by the anti-abuse screen and the account is suspended pending review. Contact support.
INVALID_CODEThe submitted OTP code is incorrect. The error message states how many attempts remain.
EXPIRED_CODEThe OTP code has expired. Request a new one via /v1/verify/send.
MAX_ATTEMPTSMaximum number of verification attempts exceeded. The session is closed.
STRIPE_ERRORThe payment page could not be created on our side. Nothing was charged — retry in a moment.
INTERNAL_ERRORInternal error on our side. Retry; contact support if the problem persists.
INVALID_PURPOSEpurpose must be marketing, transactional, otp or service.
INVALID_STATUSstatus must be granted or withdrawn.
INVALID_CHANNELchannel must be whatsapp, sms or any.
INVALID_SOURCEsource must be api, form, import or dashboard.
INVALID_EVIDENCEevidence must be a JSON object under 4 KB.
INVALID_DATEA timestamp is not ISO 8601, or recorded_at is in the future.
Limits
Constraints and quotas in the production environment.
60 to 1,200 requests / minute per key, by plan (Starter 60, Business 180, Pro 500, Enterprise 1,200), raisable on request.
1600 characters maximum per message.
Up to 10,000 messages per API call.
90 days for detailed reports.