Key for "Try it":none — samples are not sentSign up to get a test key
The key stays in this tab: it is not stored, and is sent nowhere but https://api.envoisms.ma.

Quickstart

Your first call in a minute#

The EnvoiSMS API sends SMS and WhatsApp messages and verifies phone numbers, as JSON over HTTPS. Create a sandbox key, paste a sample, run it: nothing is sent, nothing is charged.

For AI coding agents: llms.txt · MCP
Base URLhttps://api.envoisms.ma/v1
AuthenticationAuthorization: Bearer <key>
Keyssmr_… (live) · env_test_… (sandbox: nothing sent, nothing charged)
FormatJSON in and out, UTF-8, ISO 8601 timestamps (UTC)
Phone numbersE.164: +212612345678
Rate limitPer key per minute, by plan (60 to 1,200); see X-RateLimit-*
  1. Create an account, then a sandbox key in Dashboard → API keys, and export it: export ENVOISMS_API_KEY=env_test_…
  2. Paste one of the samples alongside: it runs as is with that key, sending and charging nothing.
  3. Register a webhook to receive delivery statuses and replies.
  4. Top up, create a live key (smr_…) and swap it for the sandbox key in your configuration.

1. Send an SMS

One call. With a sandbox key (env_test_…) the send is simulated: nothing leaves the platform, nothing is charged, and the response shows the price a live key would pay (simulated_cost).

Response
HTTP/1.1 202 Accepted
{
  "id": "msg_9f2c4e1a7b3d",
  "to": "+212612345678",
  "channel": "sms",
  "cascade": false,
  "status": "queued",
  "cost": { "eur": 0, "mad": 0 },
  "sandbox": true,
  "simulated_cost": { "eur": 0.0704, "mad": 0.792 },
  "segments": 1,
  "created_at": "2026-10-22T10:30:00.000Z"
}
1. Send an SMS
curl https://api.envoisms.ma/v1/messages \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "to": "+212612345678",
    "message": "Your order #1042 has shipped."
  }'

2. Send a WhatsApp template

WhatsApp sends from your own WhatsApp Business number, connected once in the dashboard (otherwise 403 WHATSAPP_NOT_CONNECTED). Outside the 24-hour window only an approved template can open the conversation; its {{1}}, {{2}}… variables go in metadata.variables, in order.

Response
HTTP/1.1 202 Accepted
{
  "id": "msg_4b81d0c2e9f7",
  "to": "+212612345678",
  "channel": "whatsapp",
  "cascade": false,
  "status": "queued",
  "cost": { "eur": 0.05778, "mad": 0.65 },
  "segments": 1,
  "created_at": "2026-10-22T10:31:00.000Z"
}
2. Send a WhatsApp template
curl https://api.envoisms.ma/v1/messages \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+212612345678",
    "channel": "whatsapp",
    "template": { "name": "order_shipped", "language": "fr" },
    "metadata": { "variables": ["Karim", "#1042"] }
  }'

3. Verify a phone number (OTP)

EnvoiSMS generates the code, delivers it, counts attempts and expires it (10 minutes by default). You store only session_id. With a sandbox key the response carries sandbox_code, so you can test /verify/check without a phone.

Response
POST /v1/verify/send → 202 Accepted
{
  "session_id": "vrf_2c7e9a14b0d3",
  "message_id": "msg_e1a07c93b5f2",
  "to": "+212612345678",
  "channel": "sms",
  "expires_at": "2026-10-22T10:42:00.000Z",
  "resend_after_seconds": 30,
  "fallback_channels": [],
  "status": "sent",
  "sandbox": true,
  "simulated": true,
  "sandbox_code": "483921"
}

POST /v1/verify/check → 200 OK
{ "session_id": "vrf_2c7e9a14b0d3", "verified": true, "verified_at": "2026-10-22T10:33:12.000Z" }
3. Verify a phone number (OTP)
# 1. Send the code
curl https://api.envoisms.ma/v1/verify/send \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+212612345678", "brand": "MyApp" }'

# 2. Check what the user typed
curl https://api.envoisms.ma/v1/verify/check \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "session_id": "vrf_…", "code": "483921" }'

Authentication

Authentication#

Every /v1 request carries an API key in the Authorization header. Create, restrict and revoke keys in Dashboard → API keys; a key is shown once, when it is created. Use it server-side only.

Two kinds of key

smr_… really sends and draws on the balance; env_test_… simulates everything, free. Same URL for both.

Permissions

Each key carries a list of permissions (send, verify, status…). Outside that list: 403 FORBIDDEN.

IP allow-list

Optional, per key. A call from any other IP gets 401 UNAUTHORIZED.

Rate-limit headers

X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time) on every response.

Request headers
POST /v1/messages HTTP/1.1
Host: api.envoisms.ma
Authorization: Bearer smr_your_live_key
Content-Type: application/json
Idempotency-Key: 6f1d2c9e-4b7a-4f3e-9d21-0c5e8a7b3f10

HTTP/1.1 202 Accepted
X-RateLimit-Limit: 180
X-RateLimit-Remaining: 179
X-RateLimit-Reset: 1792663860

Order confirmation (SMS)

Send an order confirmation by SMS#

Updated

  1. Create a sandbox key and export it: export ENVOISMS_API_KEY=env_test_…
  2. Optional: request your sender name (POST /v1/sender-ids, 15 to 30 days). Until then, omit sender_id: the default sender applies.
  3. On your "order paid" event, send the SMS with Idempotency-Key = the order id: a hook that fires twice sends one message.
  4. Register a webhook for message.delivered and message.failed; metadata.order_id comes back in every delivery.
  5. Try +212600000002 to see the failure branch, then switch to a live key (smr_…).
Was this section helpful?
Order confirmation (SMS)
curl https://api.envoisms.ma/v1/messages \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-paid" \
  -d '{
    "to": "+212612345678",
    "message": "Commande #1042 confirmée : 349 MAD. Livraison prévue jeudi.",
    "metadata": { "order_id": "1042" }
  }'

COD orders on WhatsApp

Confirm cash-on-delivery orders on WhatsApp#

Updated

On WooCommerce the EnvoiSMS plugin (v2.0.0) does all of it: each COD order waits, the customer receives a message with Confirm / Cancel buttons, and their tap updates the order.

  1. Connect your WhatsApp number in Dashboard → WhatsApp (you can keep the WhatsApp Business app on your phone).
  2. Install the plugin from github.com/envoisms/envoisms-woocommerce (WooCommerce 7.1+, PHP 8.0+) and paste your API key in WooCommerce → EnvoiSMS Maroc.
  3. Tick COD "Activer" and save: the plugin registers its webhook (message.inbound) and keeps the signing secret.
  4. Click "Créer les modèles WhatsApp" (French and Arabic): EnvoiSMS reviews them, then submits them to Meta; their status shows on the same page.
  5. Once the templates are approved, place a test cash-on-delivery order. You can follow their status through the API:
Was this section helpful?
COD orders on WhatsApp
curl "https://api.envoisms.ma/v1/templates?channel=whatsapp" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"

Marketing campaign (API)

Run a marketing campaign through the API#

Updated

  1. Create a list: POST /v1/contacts/lists {"name": "Loyal customers"} → lst_…
  2. Import contacts: POST /v1/contacts/import {"list_id", "contacts": [{"phone", "name"}]}.
  3. Record each person's proof of consent: POST /v1/consents {"phone", "purpose": "marketing", "status": "granted", "source": "form", "evidence": {…}}.
  4. Create the campaign: SMS with body (say how to unsubscribe); WhatsApp with template_id, template_variables and "consent_attested": true. scheduled_at schedules it.
  5. Launch it (POST /v1/campaigns/{id}/send), follow it with GET /v1/campaigns/{id}, pause it with /pause. Opted-out numbers (STOP) are left out automatically.
Was this section helpful?
Marketing campaign (API)
curl https://api.envoisms.ma/v1/campaigns \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Soldes de novembre",
    "channel": "sms",
    "list_id": "lst_…",
    "body": "-20% ce week-end avec le code NOV20.",
    "scheduled_at": "2026-11-07T09:00:00Z"
  }'

Two-way inbox (webhooks)

A two-way support inbox#

Updated

  1. Register a webhook for message.inbound (and message.read if you show read receipts).
  2. Verify the signature, then file the message by number: data.from is the customer, data.channel the channel.
  3. Read a customer's history with GET /v1/conversations/{phone}.
  4. Reply with POST /v1/conversations/{phone}/messages. On WhatsApp, free text is possible only within 24 hours of the customer's last message; after that, send a template (template_name) or switch to SMS.
  5. Mark the thread read with POST /v1/conversations/{phone}/mark-read. A customer STOP arrives as contact.optout and blocks later sends.
Was this section helpful?
Two-way inbox (webhooks)
curl https://api.envoisms.ma/v1/conversations/+212612345678/messages \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Bonjour, votre colis part aujourd'"'"'hui." }'

Platforms & agencies

Connect your clients' WhatsApp#

Updated

A WhatsApp number cannot be connected through the API: Meta requires the business owner to sign in themselves (Embedded Signup). So each client has its own EnvoiSMS account, which pays for its sends.

  1. The client creates its account on envoisms.ma (e-mail sign-up).
  2. It connects its number in Dashboard → WhatsApp by signing in to Meta (Embedded Signup; coexistence with the WhatsApp Business app is possible).
  3. It creates an API key limited to what you need (e.g. send, status) and hands it to you; you keep it server-side, one per client.
  4. Check the number can send: GET /v1/whatsapp/waba-connection → readiness.can_send and readiness.blockers.
  5. Create its templates (POST /v1/templates) and send with its key. Its sends, balance and webhooks stay its own.
Was this section helpful?
Platforms & agencies
curl https://api.envoisms.ma/v1/whatsapp/waba-connection \
  -H "Authorization: Bearer $CLIENT_API_KEY"

Sandbox & testing

Test without sending or paying#

Updated

Create a sandbox key in Dashboard → API keys (tick "Sandbox"). It starts with env_test_; a live key starts with smr_. Both call the same URL, https://api.envoisms.ma/v1: the key decides, not the address.

A sandbox key is validated exactly like a live key (number format, Sender ID, 24-hour window, content), but nothing is sent and nothing is charged. What a live key would have been refused for is reported in warnings rather than hidden.

  • POST /v1/messages returns sandbox: true, a zero cost and simulated_cost: the real price of the same send.
  • POST /v1/verify/send returns sandbox_code: the code the user would have received.
  • Each key sees only its own kind of traffic: GET /v1/messages with a sandbox key lists only simulated sends, and the other way round.
  • Test numbers (POST /v1/messages, sandbox key only): +212600000002 fails (message.failed, SANDBOX_TEST_FAILED); +212600000003 is undeliverable (message.undeliverable); +212600000004 is opted out (403 OPTED_OUT at request time); +212600000005 never gets a receipt (stays "sent"); every other number, +212600000001 included, is delivered (message.delivered) about 2 s after the send.
  • A sandbox key cannot be turned into a live one: create a live key when you are ready and swap the environment variable.
Was this section helpful?

SMS or WhatsApp?

Choosing the channel#

Updated

SMS is the default: omit channel. It reaches any phone, with no connected number, no template and no 24-hour window. It is the right choice for a notification or an alert to any customer.

WhatsApp ("channel": "whatsapp") needs your own connected WhatsApp Business number. To write first, or more than 24 hours after the customer's last message, use an approved template; free text is accepted only within 24 hours of a message from the customer (otherwise 400 OUT_OF_24H_WINDOW, nothing charged). WhatsApp is priced by the template's category.

Both at once: "cascade": true tries WhatsApp, then falls back to SMS if WhatsApp refuses, fails or does not confirm delivery within cascade_timeout seconds (120 by default). Only what actually went out is billed.

Verification codes: prefer POST /v1/verify/send, which handles generation, attempts, expiry and resend, over sending your own code through /v1/messages.

Was this section helpful?

Idempotency & retries

Retry without sending twice#

Updated

Add an Idempotency-Key header (up to 255 characters, e.g. a UUID or "order-1042-shipped") to POST /v1/messages and POST /v1/messages/bulk (and their /v1/waba/messages equivalents). If the call times out or your process crashes, send the same request with the same key: the send happens once, and the retry returns the original JSON body with Idempotency-Replayed: true.

  • A key is kept for 24 hours, per account. Every response below 500 is stored and replayed, 4xx errors included (e.g. 429 or 402): after a 4xx, fix the cause and use a NEW key.
  • The key is bound to the exact body, byte for byte: a retry resends the same body. A different body with the same key — even the same JSON formatted differently — returns 422 IDEMPOTENCY_KEY_REUSED.
  • A 5xx or a timeout is not stored: retry with the SAME key, backing off (1 s, 2 s, 4 s…).
  • 409 IDEMPOTENCY_IN_FLIGHT: the original request is still running; wait Retry-After and retry with the same key. 422 IDEMPOTENCY_KEY_REUSED: the key was already used with a different body.
  • 429 RATE_LIMITED: wait the number of seconds in Retry-After. X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset come with every authenticated response.
Was this section helpful?

Pagination

Paging through lists#

Updated

Lists take limit and offset and return { "data": [...], "limit", "offset", "total" }. For GET /v1/messages: 50 by default, 200 at most, newest first, filterable by status, channel, campaign_id, from_date and to_date. Add format=csv to get the whole filtered set as one file.

Was this section helpful?
Pagination
curl "https://api.envoisms.ma/v1/messages?status=failed&limit=100&offset=0" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"

Key permissions

One key, the least access#

Updated

Every key carries permissions. A call outside them returns 403 FORBIDDEN. Give each server the smallest list that works: an OTP backend needs only verify and status.

  • send — POST /v1/messages, /v1/messages/bulk, /v1/templates, /v1/conversations, /v1/lookup
  • status — GET /v1/messages, POST /v1/verify/check and the rest of /v1/verify (sessions, apps, lookup, void)
  • verify — POST /v1/verify/send, /v1/verify/resend
  • balance — GET /v1/billing/balance; billing — the rest of /v1/billing
  • contacts (contacts, lists, consents), campaigns, webhooks, optouts, analytics, api_keys, sender_id
  • WhatsApp setup (/v1/whatsapp/…): changes — connecting a number, tools, profile, relays — need a dashboard session (an API key gets 403). Only reads are open to keys, e.g. GET /v1/whatsapp/waba-connection.
Was this section helpful?

Versioning & changes

What can change, and how you will know#

Updated

The version is in the path: /v1. Inside /v1 the API only changes additively — new endpoints, optional fields, response fields, error codes, webhook events. Ignore fields you do not know. A breaking change ships as /v2 beside /v1, never in its place.

Removing anything from /v1 is announced at least 12 months ahead: by e-mail to the account owner, in the changelog, and on the route itself, whose every response then carries Deprecation (when it was announced), Sunset (when it stops) and Link to the migration guide. The only exception is a security fix, announced as early as possible. No /v1 route is deprecated today.

Was this section helpful?

Limits per endpoint

Limits, endpoint by endpoint#

Updated

Every route shares the key's per-minute limit (by default Starter 60, Business 180, Pro 500, Enterprise 1,200; X-RateLimit-* on every response; 429 RATE_LIMITED with Retry-After beyond it). Some routes add rules of their own:

  • POST /v1/messages/bulk: 10,000 messages per call.
  • POST /v1/messages and /bulk: the account's optional daily and monthly quotas (PATCH /v1/billing/quota); beyond them, 429 QUOTA_EXCEEDED until midnight UTC.
  • POST /v1/verify/send and /resend: per number, one code every 30 s on SMS and 60 s on WhatsApp (429 OTP_COOLDOWN), 5 per hour and 10 per day (429 OTP_RATE_LIMIT); 200 per day per account; outside Morocco, OTP_COUNTRY_PAUSED when too few codes are confirmed.
  • POST /v1/verify/check: 3 attempts per session by default (set per Verify app), then 429 MAX_ATTEMPTS.
  • POST /v1/inbound-webhooks/catch/{id} (no key): 120 requests per minute per webhook, 64 KB per request.
  • Your webhooks: we wait at most 10 s for your answer, then retry 3 times (1 min, 10 min, 1 h).
Was this section helpful?

Webhooks

Receive statuses and replies#

Updated

Create a webhook with POST /v1/webhooks ({"url": "https://…", "events": ["message.delivered", "message.failed"]}). The response carries the signing secret, shown once; POST /v1/webhooks/{id}/rotate-secret issues a new one. events takes an exact name, a namespace ("message.*") or "*".

Each delivery is a JSON POST {"event", "data", "timestamp"}. For a message status, data is {id, to, channel, status, error_code?, error_message?, metadata?} — metadata is what you sent. Answer 2xx within 10 seconds, then process asynchronously; de-duplicate on data.id + event, since a delivery can arrive twice.

Verify X-EnvoiSMS-Signature-256: t=<seconds>,v1=<hex>: compute HMAC-SHA256(secret, t + "." + raw body), compare it to v1 in constant time, and reject when t is more than 5 minutes old. The older X-EnvoiSMS-Signature header (sha256=<hex> of the body alone, no timestamp) is still sent; prefer the new one, which prevents replay. The event is also in X-EnvoiSMS-Event.

  • Retries: a 5xx, a 429 or a network error is retried 3 times (after 1 min, 10 min and 1 h — 4 attempts in all). Any other 4xx is not retried.
  • Log and replay: GET /v1/webhooks/logs lists the last 100 attempts; POST /v1/webhooks/logs/{log_id}/replay sends a delivery again, freshly signed. POST /v1/webhooks/{id}/test sends a sample.
  • WhatsApp relay to your server (a connected number set to "webhook" mode in the dashboard): messages and statuses arrive in WhatsApp Cloud's native shape, signed with X-Hub-Signature-256: sha256=<hex> = HMAC-SHA256(the number's webhook_secret, raw body). That secret is shown once, when it is created or rotated (rotate_webhook_secret). With no secret nothing is relayed. There is no timestamp: de-duplicate on the message id.

Events

message.sent

The message was handed to the network (carrier or WhatsApp).

message.delivered

The recipient's phone confirmed receipt.

message.read

WhatsApp: the recipient read the message (unless they turned read receipts off).

message.failed

The send failed; error_code and error_message say why (see Delivery failure codes).

message.undeliverable

The network confirmed the message cannot be delivered (number does not exist, expired at the carrier).

message.fallback

Cascade: the message moves to the next channel. channel = the new channel, previous_channel = the old one, error_code = the reason.

message.inbound

A contact wrote to you (an SMS reply or a WhatsApp message, including a button or list tap).

message.reaction

WhatsApp: a contact reacted to one of your messages with an emoji.

contact.optout / contact.optin

A contact unsubscribed (STOP) or opted back in. Sends to an unsubscribed number are refused (403 OPTED_OUT).

lead.qualified

The WhatsApp qualification bot finished qualifying a lead.

Was this section helpful?
Verify the signature
import crypto from 'node:crypto';
import express from 'express';

const app = express();

// Keep the RAW body: the signature is computed over the exact bytes received.
app.post('/webhooks/envoisms', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('X-EnvoiSMS-Signature-256') || '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto
    .createHmac('sha256', process.env.ENVOISMS_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`)
    .digest('hex');

  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const valid = parts.v1?.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
  if (!fresh || !valid) return res.sendStatus(401);

  const { event, data } = JSON.parse(req.body);
  res.sendStatus(200);              // answer fast…
  queue.push({ event, data });      // …then process (de-duplicate on data.id + event)
});
Sample delivery
POST /webhooks/envoisms HTTP/1.1
Content-Type: application/json
X-EnvoiSMS-Event: message.delivered
X-EnvoiSMS-Signature-256: t=1792663800,v1=5f0c…e9
X-EnvoiSMS-Signature: sha256=a41b…7d
User-Agent: EnvoiSMS-Webhook/1.0 (formerly SMSMaroc)

{
  "event": "message.delivered",
  "data": {
    "id": "msg_9f2c4e1a7b3d",
    "to": "+212612345678",
    "channel": "sms",
    "status": "delivered",
    "metadata": { "order_id": "1042" }
  },
  "timestamp": "2026-10-22T10:30:04.000Z"
}

Errors

Errors#

An error returns a 4xx or 5xx status and always the same envelope. Branch on error.code, never on the text: error.message is an English sentence whose wording may improve. Each code below says what to do.

HTTP/1.1 400 Bad Request
{
  "error": {
    "code": "INVALID_PHONE",
    "message": "to must be E.164 format, for example +212612345678",
    "docs": "https://envoisms.ma/en/docs#errors"
  }
}

Authentication and access

UNAUTHORIZEDHTTP 401

Authorization header missing, the key unknown or revoked, the caller's IP not on the key's allow-list, or the account not active; error.message says which. Send Authorization: Bearer <key> and check the key in Dashboard → API keys.

FORBIDDENHTTP 403

The key lacks the permission for this route (see Key permissions), or the route needs a dashboard session. Add the permission to the key or use another one.

ACCOUNT_SUSPENDEDHTTP 403

The account is suspended. Contact support; nothing can be sent until it is reviewed.

WORKSPACE_NOT_FOUNDWORKSPACE_SUSPENDEDAPI_KEY_WORKSPACE_MISMATCHHTTP 404 / 403

The workspace specified in the x-workspace-id header was not found, suspended, or conflicts with the workspace bound to this API key.

FORBIDDEN_ASSETHTTP 403

That WhatsApp number does not belong to your account. Use a phone_number_id from your own connected numbers.

PAYMENT_NOT_SETTLEDINVALID_CREDENTIALMACHINE_PAYMENTS_DISABLEDHTTP 402 / 400 / 501

Machine payments (Authorization: Payment …): the deposit has not settled yet (wait, then retry), the credential does not decode, or the feature is off.

Malformed request

INVALID_JSONHTTP 400

The body is not valid JSON. Send Content-Type: application/json and a JSON object.

MISSING_FIELDMISSING_FIELDSINVALID_FIELDMISSING_BODYMISSING_MESSAGEMISSING_URLHTTP 400

A required field is missing or out of range; error.message names the field. Fix it and resend.

INVALID_PHONEHTTP 400

The number is not E.164. Send + country code then the number, no spaces: +212612345678.

INVALID_URLHTTP 400

A URL is not https:// or not valid (webhook, media, button). Use a public https URL.

INVALID_CHANNELCHANNEL_NOT_CONFIGUREDCHANNEL_DISABLEDUNSUPPORTED_CHANNELHTTP 400

Unknown or unavailable channel. For /v1/messages channel is sms (default) or whatsapp; for /v1/consents it is whatsapp, sms or any.

INVALID_PURPOSEINVALID_STATUSINVALID_SOURCEINVALID_EVIDENCEINVALID_DATEHTTP 400

Consents: purpose = marketing | transactional | otp | service; status = granted | withdrawn; source = api | form | import | dashboard; evidence = a JSON object under 4 KB; dates ISO 8601, recorded_at never in the future.

INVALID_PERMISSIONSSANDBOX_IMMUTABLEHTTP 400

API keys: give at least one valid permission; a key never switches between sandbox and live — create a new one.

NOT_FOUNDLIST_NOT_FOUNDLEAD_NOT_FOUNDTEMPLATE_NOT_FOUNDAPP_NOT_FOUNDSHEET_NOT_FOUNDINVALID_MESSAGE_IDHTTP 400 / 404

The resource does not exist for this account (or this key mode: a sandbox key does not see live objects), or the requested identifier is invalid (e.g. literal null or undefined). Check the id and the key.

METHOD_NOT_ALLOWEDHTTP 405

That HTTP method does not exist on this route. See the endpoint reference.

UNSUPPORTED_MEDIA_TYPEPAYLOAD_TOO_LARGEFILE_TOO_LARGEINVALID_FILE_TYPEINVALID_FORM_DATAMISSING_FILEEMPTY_SHEETPARSE_FAILEDMAPPING_ERRORFETCH_FAILEDINVALID_PATHNO_FILE_PROVIDEDNO_DATA_PROVIDEDUPLOAD_FAILEDHTTP 400 / 413 / 415 / 502

File or import refused: unsupported type, too large, empty, unrecognised columns or an unreachable URL. error.message says which; fix the file and resend.

Sending (SMS and common)

INSUFFICIENT_BALANCEHTTP 402

Balance too low for this send. Top up in Dashboard → Billing; nothing was charged.

INTERNATIONAL_REQUIRES_TOPUPHTTP 403

Sends outside Morocco are for accounts that have topped up at least once. Make a first top-up.

QUOTA_EXCEEDEDHTTP 429

The daily quota set on your account is reached; it resets at midnight UTC. Raise it in the dashboard if needed. After this 429, retry with a new Idempotency-Key.

OPTED_OUTHTTP 403

This recipient unsubscribed from your messages (STOP). Do not resend; remove them from your lists. They can opt back in themselves.

SENDER_ID_TOO_LONGSENDER_ID_TOO_SHORTSENDER_ID_INVALIDHTTP 400

An alphanumeric Sender ID is 3 to 11 characters (letters, digits, space). Fix it, or omit it to use the default sender.

SENDER_ID_GENERICHTTP 400

Generic Sender ID (INFO, ALERT, SERVICE…), not a brand. Use your brand name or omit it.

SENDER_ID_PROTECTED_BRANDHTTP 403

The Sender ID names or resembles a protected institution (bank, carrier, public body). Reserved to the account approved for that exact name.

SENDER_ID_NOT_APPROVEDSENDER_ID_PENDINGSENDER_ID_IN_REVIEWSENDER_ID_REJECTEDSENDER_ID_APPROVEDHTTP 403 / 409

Sender ID registration: not approved yet, under review, rejected, or already approved (409). Follow its state in Settings → Sender IDs.

SENDER_ID_NOT_REGISTEREDHTTP 403

No longer returned since 2026-09-27: an unregistered Sender ID is sent automatically at the standard rate instead. Kept so code that checks for it stays valid.

SENDER_NOT_ASSIGNED_TO_WORKSPACEHTTP 403

The specified Sender ID is not assigned to this workspace. Assign it in Settings → Workspaces.

NO_SENDER_ID_ASSIGNEDHTTP 422

No Sender ID is assigned to this workspace. Assign an approved Sender ID in Settings → Workspaces.

WORKSPACE_BALANCE_EXHAUSTEDHTTP 402

The balance allocated to this workspace is exhausted. Allocate more balance from the parent account in Settings → Workspaces.

SPAM_OR_PHISHING_DETECTEDCONTENT_BLOCKEDHTTP 400

Content refused: a phishing link, a message speaking as a bank, carrier or public body, or an acceptable-use violation. The account is suspended pending review. To appeal: POST /v1/moderation/appeals with error.appeal.id, error.appeal.token and a note (no API key needed).

CONTENT_REFUSEDHTTP 400

Content refused by the anti-abuse screen (first refusal). Nothing was sent and the account stays active; a second refusal suspends it. Reword, or appeal via POST /v1/moderation/appeals.

INVALID_APPEALAPPEAL_NOT_FOUNDAPPEAL_ALREADY_FILEDHTTP 400 / 404 / 409

Appeal: id or token missing or wrong, or an appeal is already open for this refusal.

EMPTY_BATCHTOO_MANY_MESSAGESHTTP 400

POST /v1/messages/bulk takes 1 to 10,000 messages per call. Split larger batches.

SEND_PERSIST_FAILEDHTTP 502

The message could not be recorded; nothing was sent and nothing was charged. Retry with the same Idempotency-Key.

WhatsApp

WHATSAPP_NOT_CONNECTEDHTTP 403

No WhatsApp Business number connected to the account. Connect one in the dashboard; for a plain text, omit channel (SMS). Nothing was charged.

OUT_OF_24H_WINDOWHTTP 400

Free-form WhatsApp message to a contact who has not written to you in 24 hours. Send an approved template, or an SMS. Nothing was charged.

CONTACT_HAS_NO_PHONEHTTP 400

This contact uses a WhatsApp username and has not shared a phone number: they can only be reached on WhatsApp, not by SMS. Nothing was charged.

WHATSAPP_ONLY_FIELDBUTTONS_NOT_SUPPORTEDINTERACTIVE_NOT_SUPPORTEDHTTP 400

A WhatsApp-only field (buttons, interactive, reaction, contacts, location, sticker, context) was sent on SMS. Add "channel": "whatsapp" or drop the field.

CONFLICTING_MESSAGE_TYPESCASCADE_NOT_SUPPORTEDINVALID_MESSAGEHTTP 400

A message carries one content type; reaction, contacts, location and sticker combine with nothing and have no SMS equivalent (no cascade).

INVALID_REACTIONINVALID_CONTEXTINVALID_CONTACTSINVALID_LOCATIONINVALID_MEDIAHTTP 400

Malformed WhatsApp object: reaction = {message_id, emoji}; context = {message_id}; contacts = 1–20 cards with name.formatted_name; location = numeric latitude/longitude; media = "id" OR an https "link", no caption on audio/sticker.

INVALID_INTERACTIVEINVALID_INTERACTIVE_TYPEMISSING_INTERACTIVE_ACTIONINTERACTIVE_BODY_TOO_LONGINVALID_CTA_URL_ACTIONMISSING_CTA_DISPLAY_TEXTCTA_DISPLAY_TEXT_TOO_LONGINVALID_LOCATION_REQUEST_ACTIONINVALID_FLOW_ACTIONMISSING_FLOW_IDMISSING_FLOW_CTAFLOW_CTA_TOO_LONGMISSING_CAROUSEL_CARDSTOO_MANY_CAROUSEL_CARDSINVALID_PRODUCT_ACTIONINVALID_PRODUCT_LISTMISSING_ORDER_ACTIONMISSING_ORDER_CURRENCYMISSING_ORDER_ITEMSMISSING_ORDER_PARAMETERSMISSING_REFERENCE_IDINVALID_TOTAL_AMOUNTMISSING_PHONE_NUMBERHTTP 400

Malformed interactive message (list, buttons, cta_url, flow, carousel, product, order, location_request). error.message names the faulty field and the WhatsApp limit it broke.

MISSING_BUTTONS_ARRAYINVALID_BUTTONINVALID_BUTTON_COUNTINVALID_BUTTON_TEXTINVALID_BUTTON_TYPEINVALID_REPLY_BUTTONBUTTON_TEXT_TOO_LONGBUTTON_TITLE_TOO_LONGTOO_MANY_BUTTONSMISSING_COPY_CODEMISSING_LIST_BUTTONLIST_BUTTON_TOO_LONGMISSING_LIST_SECTIONSTOO_MANY_SECTIONSSECTION_TITLE_TOO_LONGINVALID_SECTION_ROWSTOO_MANY_ROWSMISSING_ROW_IDMISSING_ROW_TITLEROW_ID_TOO_LONGROW_TITLE_TOO_LONGROW_DESC_TOO_LONGHTTP 400

Buttons or list outside WhatsApp's limits: at most 3 buttons, 20-character button titles, 10 sections and 10 rows, 24-character row titles, 72-character descriptions. error.message gives the exact limit.

META_SERVICE_UNAVAILABLEHTTP 502

WhatsApp was briefly unavailable and the send failed after automatic retries. Retry in a few minutes.

Templates and campaigns

TEMPLATE_PENDING_REVIEWTEMPLATE_REJECTED_BY_REVIEWHTTP 409

The template is waiting for EnvoiSMS review (it goes to Meta once approved), or that review rejected it: edit it to send it back to review.

TEMPLATE_EXISTSTEMPLATE_ALREADY_ON_METATEMPLATE_EDIT_NOT_ALLOWEDTEMPLATE_IMMUTABLE_FIELDHTTP 400 / 409

A template with that name exists, is already at Meta (sync it), cannot be edited in its current state, or you are changing a field Meta freezes (name, language, the category of an approved template) — create a new one.

INVALID_LANGUAGEINVALID_META_TEMPLATEINVALID_AUTH_TEMPLATEMISSING_SAMPLE_VALUESMISSING_HEADER_EXAMPLEINVALID_HEADER_EXAMPLEHEADER_EXAMPLE_INVALID_SIZEHEADER_EXAMPLE_INVALID_TYPEHEADER_EXAMPLE_UNREACHABLEHTTP 400

The template breaks Meta's rules: unknown language, missing variable samples, a header media that is missing, unreachable, the wrong type or too large, or a non-conforming authentication template. error.message says which.

INVALID_WABAWHATSAPP_TOKEN_EXPIREDMETA_API_ERRORMETA_SUBMIT_FAILEDMETA_UPLOAD_FAILEDMETA_NOT_CONFIGUREDHTTP 400 / 502 / 503

Meta refused the operation on your WhatsApp Business account: a waba_id that is not yours, an expired connection token (reconnect the account in the dashboard) or a Meta error. Retry; if it persists, contact support.

TEMPLATE_NOT_APPROVEDMISSING_TEMPLATEMISSING_TEMPLATE_PARAMSHTTP 400

WhatsApp campaign or conversation: the template is not approved, is missing, or variables are missing. MISSING_TEMPLATE_PARAMS lists what to supply in template_variables (_header_media_url, _button_<i>, or "static:value").

INVALID_WABA_SENDERMISSING_LISTEMPTY_LISTLIST_IN_USEHTTP 400

Campaign: the WhatsApp sender is not connected to your account, the list is missing or has no eligible recipient, or the list is used by an active campaign (and cannot be deleted).

ALREADY_RUNNINGCAMPAIGN_RUNNINGCAMPAIGN_NOT_EDITABLENOT_PAUSEDNOT_RUNNINGHTTP 400 / 409

The campaign's state does not allow this action: only a draft or scheduled campaign can be edited, and a running campaign must be paused before it is deleted.

Verification (OTP)

INVALID_CODEHTTP 400

Wrong code. error.message states the attempts left; let the user try again.

MAX_ATTEMPTSHTTP 429

Too many attempts: the session is closed. Start a new one with /v1/verify/send.

SESSION_NOT_FOUNDSESSION_EXPIREDINVALID_STATEINVALID_SESSION_DATAINVALID_SESSION_IDHTTP 400 / 404 / 409

Session unknown, expired, invalid ID (e.g. literal null or undefined), already verified or voided, or created by a key of the other mode (sandbox/live). Start a new verification, using the same key for send, resend and check.

OTP_COOLDOWNOTP_RATE_LIMITHTTP 429

A code was just sent to this number (30 s on SMS, 60 s on WhatsApp), or too many codes went there this hour. Wait Retry-After; offer "Resend" through /v1/verify/resend.

OTP_RESEND_LIMIT_EXCEEDEDHTTP 429

Maximum resend attempts (4 dispatches) reached for this verification session. Wait for the session to expire before starting a new one.

OTP_COUNTRY_PAUSEDHTTP 429

Codes to that country (outside Morocco) are paused for your account until 00:00 UTC: too few of today's codes there were confirmed, the pattern of automated abuse. Retry-After carries the seconds left.

COUNTRY_NOT_ALLOWEDHTTP 403

Codes to that country are not open for your account or Verify app. Contact support to open it.

WHATSAPP_UNAVAILABLEDELIVERY_FAILEDINVALID_CODE_TYPEBAD_GATEWAYHTTP 400 / 502 / 503

The code could not go out on WhatsApp (unavailable for the account, refused, or a non-numeric code). Nothing was charged; resend with "channel": "sms".

HLR_UNAVAILABLEHTTP 503

The HLR query (/v1/hlr) could not be made: the service is momentarily unavailable or rate-limited. Retry in a few seconds; nothing was charged. A 502 BAD_GATEWAY on this route means the network did not answer — same rule, nothing is charged.

LOOKUP_FAILEDLOOKUP_UNAVAILABLEHTTP 500 / 503

The carrier lookup (/v1/verify/lookup) failed or is unavailable. Retry later; nothing was charged.

Rate limits and idempotency

RATE_LIMITEDTOO_MANY_REQUESTSRATE_LIMIT_EXCEEDEDHTTP 429

The key's per-minute request limit was exceeded. Wait Retry-After (seconds); retrying sooner only uses up the next window.

INVALID_IDEMPOTENCY_KEYHTTP 400

Idempotency-Key is longer than 255 characters. Use a UUID or a short business identifier.

IDEMPOTENCY_IN_FLIGHTHTTP 409

The original request with this key is still running. Wait Retry-After and retry with the same key.

IDEMPOTENCY_KEY_REUSEDHTTP 422

This Idempotency-Key was already used with a different request body. A key names one request: generate a new one.

REPLAY_UNAVAILABLEHTTP 422

This webhook delivery has no stored payload to resend. It cannot be replayed.

Account and billing

LIMIT_EXCEEDEDHTTP 403

Maximum number of API keys or webhooks reached for your plan. Revoke one or change plan.

INVALID_AMOUNTINVALID_PACKINVALID_PAYMENT_METHODUNSUPPORTED_METHODALREADY_COMPLETEDTRANSACTION_CANCELLEDRECEIPT_NOT_AVAILABLEINVOICE_NOT_AVAILABLESTRIPE_INVOICES_UNAVAILABLENO_STRIPE_CUSTOMERINVALID_THRESHOLDINVALID_CYCLEINVALID_CYCLE_LIMITNO_PAID_TOPUPNO_FIELDSHTTP 400 / 403 / 404 / 409 / 410

Top-up or auto-recharge: invalid amount, threshold, cycle cap, cycle, pack or payment method, first manual top-up required, transaction cancelled or already completed, or no invoice available (yet).

STRIPE_ERRORPORTAL_ERRORCUSTOMER_CREATION_FAILEDSTRIPE_NOT_CONFIGUREDPAYMENT_METHOD_NOT_CONFIGUREDNOT_AVAILABLEINVOICE_TEMPORARILY_UNAVAILABLEINVOICE_BEING_GENERATEDINVALID_SIGNATUREHTTP 401 / 500 / 502 / 503

The payment or invoice page could not be obtained on our side. Nothing was charged; retry in a moment.

Server side

INTERNAL_ERRORDB_ERRORSERVICE_UNAVAILABLEBAD_REQUESTHTTP 400 / 500 / 503

An error on our side. Retry with backoff (and the same Idempotency-Key for a send); contact support if it persists.

Delivery failure codes

An accepted message (202) can still fail later. Its error_code — on GET /v1/messages/{id} and in the message.failed webhook — is one of these. A WhatsApp message refused before it went out is refunded automatically.

META_WINDOW_EXPIRED

More than 24 hours since the contact's last message. Send an approved template or an SMS.

META_UNDELIVERABLE

The number cannot be reached on WhatsApp. Not retried automatically: use SMS.

META_MARKETING_LIMIT

WhatsApp's per-recipient marketing limit. Do not resend right away.

META_RATE_LIMITED / META_PAIR_RATE_LIMITED

The number's throughput was exceeded, or too many messages went to this contact. Failed after automatic retries; slow down.

META_POLICY_BLOCKED / META_ACCOUNT_LOCKED / META_PAYMENT_ISSUE / META_PHONE_NOT_REGISTERED

The sending WhatsApp number or account is restricted, locked, without a valid Meta payment method, or not registered. Check WhatsApp Manager; your EnvoiSMS funds are not the issue.

META_TEMPLATE_NOT_FOUND / META_TEMPLATE_PARAM_MISMATCH / META_TEMPLATE_PARAM_FORMAT

Template missing in that language or not approved yet, the wrong number of variables, or a variable in the wrong format (header media, button).

META_UNSUPPORTED_TYPE

This message type is not supported by WhatsApp for this recipient.

META_GENERIC_ERROR / META_SERVICE_UNAVAILABLE

A temporary WhatsApp error that persisted after automatic retries. Resend in a few minutes.

CASCADE_TIMEOUT

Cascade: no delivery report within cascade_timeout; the message moved to the next channel (message.fallback event).

Was this section helpful?

Limits

Limits#

Requests

Per key per minute. A new key's default by plan: Starter 60, Business 180, Pro 500, Enterprise 1,200. Beyond it: 429 RATE_LIMITED with Retry-After.

Bulk send

Up to 10,000 messages per POST /v1/messages/bulk call.

Verification

4- to 8-digit code (6 by default), valid 60 s to 30 min (10 min by default). Resend allowed after 30 s on SMS, 60 s on WhatsApp.

Idempotency

Idempotency-Key of up to 255 characters, kept 24 hours per account.

SDKs

Client libraries#

Optional: every sample on this page runs as plain HTTP, with no dependency.

Node.jsTypeScript · JavaScript
npm install envoisms
GitHub repository
PythonPython 3.8+
pip install envoisms
GitHub repository
PHPPHP 8.1+
composer require envoisms/envoisms-php
GitHub repository
Laravel OTPLaravel 9–11
composer require envoisms/laravel-otp
GitHub repository
GoGo 1.21+
go get github.com/n4ouri/envoisms-go
GitHub repository
MCP serverClaude, Cursor, agents
npx -y @envoisms/mcp-server
GitHub repository
DocsMessages/messages
POSThttps://api.envoisms.ma/v1/messages
Bearer

Send a message

Queues one SMS or WhatsApp message to one recipient and debits its price. Returns 202 with the message id; the delivery outcome arrives later by webhook or through GET /v1/messages/:id.

Request body (JSON)
FieldTypeRequirementDescription
tostringRequiredRecipient number in E.164 format (+212612345678).
messagestringRequiredMessage text, at most 1,600 characters (longer text is cut). Alias: body. Optional on WhatsApp when the request carries a template, media, a reaction, contacts, a location or an interactive message.
sender_idstringOptionalSMS: the name shown to the recipient (letters, digits and spaces, 3 to 11 characters; a generic header such as INFO or a bank’s name is refused). Omitted, the SMS goes out under our shared sender EnvoiSMS at 0.792 MAD. Your own name works with no registration at 1.70 MAD until it is validated with your RC, then at the standard rate once validated. WhatsApp: the connected number that sends. Aliases: from, sender.
channelstringOptional"sms" (default) or "whatsapp". "whatsapp" sends from your own connected WhatsApp Business number (otherwise 403 WHATSAPP_NOT_CONNECTED). Unless it is a template, a WhatsApp message is accepted only if the contact wrote to you in the last 24 hours; otherwise 400 OUT_OF_24H_WINDOW, with nothing charged.
cascadeboolean | string[]Optionaltrue: tries WhatsApp, then falls back to SMS — at once if WhatsApp refuses the message or reports it failed, or when no delivery report arrives within cascade_timeout. Also accepts an ordered list such as ["whatsapp", "sms"]. Only a send that actually went out is billed; a WhatsApp message still waiting for its report did go out, so it is billed along with its SMS fallback and refunded if WhatsApp later reports it failed.
cascade_timeoutintegerOptionalWith cascade: seconds to wait for a delivery report before the next channel, from 30 to 43200. Default: 120. Give your WhatsApp template a time-to-live (TTL) no longer than this, so a phone that comes back online does not receive both messages.
templateobjectOptional(WhatsApp) An approved template of your account: {"name", "language", "components"?}. Body variable values go in metadata.variables. The only message type allowed outside the 24-hour window; priced by the template’s category.
metadataobjectOptionalKey-value pairs stored with the message and returned in webhooks (not by GET /v1/messages). Keys the platform reads: variables (template values), media_url and media_type (the template’s media header), purpose: "otp" (see notes).
buttonsarrayOptional(WhatsApp) Up to 3 buttons {"type", "text"}; text at most 25 characters. type "copy" needs copy_code, "url" needs url (http or https), "call" needs phone_number.
interactiveobjectOptional(WhatsApp) Interactive message in WhatsApp’s format: list ("list"), reply buttons ("button", 1 to 3), form ("flow"), link button ("cta_url") or location request ("location_request_message").
image | video | audio | document | stickerobjectOptional(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.
reactionobjectOptional(WhatsApp) {"message_id": wamid, "emoji": "👍"} — reacts to a message of the conversation; an empty emoji removes the reaction. Not billed.
contactsarrayOptional(WhatsApp) Contact cards (at most 20): name.formatted_name required; phones, emails, urls, addresses, org, birthday optional.
locationobjectOptional(WhatsApp) Location pin: {"latitude", "longitude", "name"?, "address"?}.
contextobjectOptional(WhatsApp) {"message_id": wamid} — replies quoting an earlier message. Valid on every type except reaction.
phone_number_idstringOptional(WhatsApp) Meta id of the connected number that sends; defaults to your default number.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • Idempotency: send an Idempotency-Key header (at most 255 characters) to retry a request without sending twice. For 24 hours the same key with the same body returns the original response (header Idempotency-Replayed: true); with a different body, 422 IDEMPOTENCY_KEY_REUSED; while the first request is still running, 409 IDEMPOTENCY_IN_FLIGHT.
  • Sandbox key (env_test_…): nothing is sent or debited; cost is 0, and the response adds "sandbox": true, "simulated_cost" (the price a live key would have paid) and, where relevant, "warnings" (what a live key would have been refused, such as a closed 24-hour window).
  • One-time codes: /v1/verify/send handles the whole cycle 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 under 10 minutes old, the platform resends it once over another SMS route, under the same message id and at no extra cost.
  • Common errors: 400 INVALID_PHONE, 400 OUT_OF_24H_WINDOW, 400 SENDER_ID_GENERIC, 402 INSUFFICIENT_BALANCE, 403 WHATSAPP_NOT_CONNECTED, 403 SENDER_ID_PROTECTED_BRAND. A refused request is never debited.
  • Formatting: line breaks (\n) work on every channel. SMS is plain text; WhatsApp renders *bold*, _italics_ and ~strikethrough~.
POSThttps://api.envoisms.ma/v1/messages
curl -X POST "https://api.envoisms.ma/v1/messages" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+212612345678",
    "message": "Your order #1042 has shipped."
  }'
{
  "id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
  "to": "+212612345678",
  "channel": "sms",
  "cascade": false,
  "status": "queued",
  "cost": { "eur": 0.072, "mad": 0.792 },
  "segments": 1,
  "created_at": "2026-10-22T09:14:03.512Z"
}
DocsMessages/messages/bulk
POSThttps://api.envoisms.ma/v1/messages/bulk
Bearer

Bulk send

Queues up to 10,000 messages in one call, each with its own recipient and text. Returns 202 with a batch_id, the accepted messages and, where any, the refused ones.

Request body (JSON)
FieldTypeRequirementDescription
messagesarrayRequiredArray of 1 to 10,000 objects: "to" (E.164), "message" (or "body"), and optionally "metadata" and "buttons" (WhatsApp).
sender_idstringOptionalSender for the whole batch (same rules as POST /v1/messages). Aliases: from, sender.
channelstringOptional"sms" (default) or "whatsapp". On WhatsApp a bulk send carries no template: every recipient must be inside their 24-hour window.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • The batch is validated as a whole before anything is sent: a single invalid entry fails the request with 400 INVALID_MESSAGE, listing the first 20 faulty entries (index and rule). The balance is checked for the whole batch (402 INSUFFICIENT_BALANCE).
  • "rejected" lists entries refused one by one (opted out, closed 24-hour window, …) with their code; they are not debited. It appears only when there are any. "estimated_cost" is the batch estimate before refusals.
  • Idempotency-Key is accepted, with the same rules as POST /v1/messages.
POSThttps://api.envoisms.ma/v1/messages/bulk
curl -X POST "https://api.envoisms.ma/v1/messages/bulk" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {
        "to": "+212611111111",
        "message": "Your appointment is tomorrow at 10:00."
      },
      {
        "to": "+212622222222",
        "message": "Your appointment is tomorrow at 11:30."
      }
    ]
  }'
{
  "batch_id": "batch_9a3c5e7f1b2d4c6e8a0f2b4d",
  "total": 2,
  "channel": "sms",
  "estimated_cost": { "eur": 0.144, "mad": 1.584 },
  "messages": [
    { "id": "msg_1a2b3c4d5e6f7a8b9c0d1e2f", "to": "+212611111111", "status": "queued" },
    { "id": "msg_3c4d5e6f7a8b9c0d1e2f3a4b", "to": "+212622222222", "status": "queued" }
  ],
  "rejected": [
    { "to": "+212633333333", "code": "OPTED_OUT", "message": "This number opted out of messages from your account" }
  ]
}
DocsMessages/messages/typing
POSThttps://api.envoisms.ma/v1/messages/typing
Bearer

WhatsApp typing indicator

Marks a received WhatsApp message as read and shows its sender "typing…" while you prepare the reply (until you reply, or about 25 seconds). Nothing is stored or billed.

Request body (JSON)
FieldTypeRequirementDescription
message_idstringRequiredWhatsApp id (wamid) of the received message you are answering.
typing_indicatorbooleanOptionalfalse sends the read receipt only. Default: true.
phone_number_idstringOptionalConnected number that received the message; defaults to your default number.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • With a sandbox key nothing reaches WhatsApp and the response is {"status": "simulated", "sandbox": true, "message_id", "typing_indicator"}.
POSThttps://api.envoisms.ma/v1/messages/typing
curl -X POST "https://api.envoisms.ma/v1/messages/typing" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message_id": "wamid.HBgMMjEyNjEyMzQ1Njc4FQIAEhg",
    "typing_indicator": true
  }'
{
  "status": "ok",
  "message_id": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg",
  "typing_indicator": true
}
DocsMessages/messages
GEThttps://api.envoisms.ma/v1/messages
Bearer

List messages

Returns the account’s messages, newest first, with filters and pagination. format=csv exports the whole filtered set.

Query parameters
ParameterTypeRequirementDescription
limitintegerOptionalNumber of results: 50 by default, at most 200.
offsetintegerOptionalNumber of results to skip (pagination). Default: 0.
statusstringOptionalqueued, sent, delivered, read (WhatsApp), failed, undeliverable or unconfirmed.
channelstringOptionalsms or whatsapp.
campaign_idstringOptionalMessages of one campaign (cmp_…).
from_datestringOptionalCreated on or after this date (UTC). Write it YYYY-MM-DD: it is compared with created_at, formatted "YYYY-MM-DD HH:MM:SS".
to_datestringOptionalCreated up to this date (UTC), same comparison. To include a whole day, pass the following day.
formatstringOptionalcsv: downloads the whole filtered set (up to the export cap, stated in the file) instead of one page.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: status.
  • A key sees only messages of its own mode: a sandbox key lists simulated sends only, a live key real sends only.
GEThttps://api.envoisms.ma/v1/messages
curl -X GET "https://api.envoisms.ma/v1/messages?limit=50" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
      "campaign_id": null,
      "to": "+212612345678",
      "channel": "sms",
      "body": "Votre commande 4512 est expédiée.",
      "sender_id": "MABOUTIQUE",
      "unicode": 0,
      "segments": 1,
      "status": "delivered",
      "error_code": null,
      "error_message": null,
      "cost_eur": 0.072,
      "cost_mad": 0.792,
      "scheduled_at": null,
      "sent_at": "2026-10-22 09:14:05",
      "delivered_at": "2026-10-22 09:14:09",
      "read_at": null,
      "failed_at": null,
      "created_at": "2026-10-22 09:14:03",
      "operator": "Maroc Telecom",
      "sandbox": 0
    }
  ],
  "limit": 50,
  "offset": 0,
  "total": 1
}
DocsMessages/messages/:id
GEThttps://api.envoisms.ma/v1/messages/:id
Bearer

Retrieve a message

Returns one message and its delivery state. 404 NOT_FOUND when the id does not belong to the account or to the key’s mode.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: status.
  • Statuses: queued, sent, delivered, read (WhatsApp), failed, undeliverable, unconfirmed. "unconfirmed" means the operator neither confirmed nor denied delivery; a receipt arriving later can still replace it.
  • The metadata given at send time is not returned here; it travels with the webhooks.
GEThttps://api.envoisms.ma/v1/messages/:id
curl -X GET "https://api.envoisms.ma/v1/messages/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
  "campaign_id": null,
  "to": "+212612345678",
  "channel": "sms",
  "body": "Votre commande 4512 est expédiée.",
  "sender_id": "MABOUTIQUE",
  "unicode": 0,
  "segments": 1,
  "status": "delivered",
  "error_code": null,
  "error_message": null,
  "cost_eur": 0.072,
  "cost_mad": 0.792,
  "scheduled_at": null,
  "sent_at": "2026-10-22 09:14:05",
  "delivered_at": "2026-10-22 09:14:09",
  "read_at": null,
  "failed_at": null,
  "created_at": "2026-10-22 09:14:03",
  "operator": "Maroc Telecom",
  "sandbox": 0
}
DocsTemplates/templates
GEThttps://api.envoisms.ma/v1/templates
Bearer

List templates

Returns the account’s active templates (SMS and WhatsApp), newest first, whatever their status.

Query parameters
ParameterTypeRequirementDescription
channelstringOptionalsms, whatsapp, telegram or any.
categorystringOptionalmarketing, utility or authentication.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • "variables" is a JSON string describing the template’s structure: variables, sample values, header, footer, buttons, carousel and review.
  • WhatsApp statuses: pending_admin (EnvoiSMS review), pending_meta, approved, rejected, paused, disabled, in_appeal, flagged, deleted_on_meta. An SMS template is approved as soon as it is created.
GEThttps://api.envoisms.ma/v1/templates
curl -X GET "https://api.envoisms.ma/v1/templates" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
      "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
      "name": "order_shipped",
      "channel": "whatsapp",
      "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
      "variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
      "category": "utility",
      "active": 1,
      "created_at": "2026-10-20 14:02:11",
      "status": "approved",
      "rejection_reason": null,
      "meta_template_id": "1203948576102938",
      "language": "fr",
      "quality_score": "GREEN",
      "quality_updated_at": "2026-10-21 08:00:00",
      "waba_id": null
    }
  ]
}
DocsTemplates/templates
POSThttps://api.envoisms.ma/v1/templates
Bearer

Create a template

Creates an SMS template (usable at once) or a WhatsApp template (reviewed by EnvoiSMS, then submitted to Meta on your own WhatsApp Business Account). Returns 201 with the created row.

Request body (JSON)
FieldTypeRequirementDescription
namestringRequiredTemplate name. Lowercased; any character other than a-z, 0-9 and _ becomes _.
channelstringRequiredsms, whatsapp, telegram or any.
bodystringRequiredTemplate text. WhatsApp: at most 1,024 characters; variables either all numbered from {{1}} and consecutive, or all named in lowercase ({{first_name}}); never at the very start or end, never adjacent. Not required for a WhatsApp "authentication" template, whose text Meta fixes.
categorystringOptionalmarketing (default), utility or authentication. otp maps to authentication, transactional to utility. Meta may reassign the category at review.
languagestringOptional(WhatsApp) Meta language code, such as fr, ar, en_US, es. Default: fr.
sample_valuesobject | string[]Optional(WhatsApp) One example value per variable, required by Meta: {"1": "Amine"} or ["Amine"]. Without it, 400 MISSING_SAMPLE_VALUES.
variablesstring[]Optional(SMS) Names of the text’s variables, stored as given.
header_typestringOptional(WhatsApp) none (default), text, image, video or document. A text header takes header_text (60 characters, at most one variable, with header_sample); a media header requires header_example_url.
header_text | header_sample | header_example_urlstringOptional(WhatsApp) Header text; example value of its variable; public https link to a sample file for an image, video or document header (400 MISSING_HEADER_EXAMPLE when missing).
footerstringOptional(WhatsApp) Footer, at most 60 characters.
buttonsarrayOptional(WhatsApp) Up to 10 buttons {"type", "text"} (25 characters). Types: quick_reply; url (with an http(s) url, and url_example when the link ends in a variable); phone_number (with phone_number); copy_code (with copy_code); flow (with flow_id, the id of one of your published WhatsApp Flows, and navigate_screen, "LEAD" by default).
carouselobjectOptional(WhatsApp, marketing or utility) {"header_type": "image" | "video", "cards": [...]}: 2 to 10 cards, each {"header_example_url" (https), "body" (160 characters, no variable), "buttons" (1 or 2, quick_reply or url without a variable)}. Every card carries the same buttons. A carousel template has no header, footer or buttons of its own.
add_security_recommendation | code_expiration_minutes | otp_type | supported_appsmixedOptional(WhatsApp, authentication) Meta’s security notice (boolean); code validity, 1 to 90 minutes; COPY_CODE (default), ONE_TAP or ZERO_TAP; supported_apps [{"package_name", "signature_hash" (11 characters)}], required for ONE_TAP and ZERO_TAP. ZERO_TAP also requires zero_tap_terms_accepted: true.
waba_idstringOptional(WhatsApp) Target WhatsApp Business Account when the account has several; must be one of your connected accounts (otherwise 400 INVALID_WABA).
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • Review: a WhatsApp template is created as pending_admin. Automatic checks (impersonation of a bank, phishing link, throwaway link host, inactive account) run at once; when they find nothing, the template is submitted to Meta (pending_meta) and the response already shows it. Otherwise it stays pending_admin for a person to review. Only Meta approves it (approved) and sets its final category.
  • Errors: 400 MISSING_FIELD, INVALID_CHANNEL, INVALID_LANGUAGE, INVALID_META_TEMPLATE, MISSING_SAMPLE_VALUES, MISSING_HEADER_EXAMPLE, INVALID_AUTH_TEMPLATE, INVALID_WABA; 409 TEMPLATE_EXISTS (same name and language already active; a rejected template of the same name is replaced).
POSThttps://api.envoisms.ma/v1/templates
curl -X POST "https://api.envoisms.ma/v1/templates" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order_shipped",
    "channel": "whatsapp",
    "category": "utility",
    "language": "fr",
    "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
    "sample_values": [
      "Karim",
      "#1042"
    ]
  }'
{
  "id": "tpl_8e2a4c6b1d3f5a7c9e0b2d4f",
  "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
  "name": "catalogue_automne",
  "channel": "whatsapp",
  "body": "Découvrez notre sélection d’automne, {{1}} !",
  "variables": "{\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[],\"carousel\":{\"header_type\":\"image\",\"cards\":[{\"header_example_url\":\"https://example.ma/veste.jpg\",\"body\":\"Veste en laine\",\"buttons\":[{\"type\":\"url\",\"text\":\"Voir\",\"url\":\"https://example.ma/veste\"}]},{\"header_example_url\":\"https://example.ma/pull.jpg\",\"body\":\"Pull en coton\",\"buttons\":[{\"type\":\"url\",\"text\":\"Voir\",\"url\":\"https://example.ma/pull\"}]}]},\"variables\":[\"1\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\"}}",
  "category": "marketing",
  "active": 1,
  "created_at": "2026-10-22 09:20:41",
  "status": "pending_meta",
  "rejection_reason": null,
  "meta_template_id": "1203948576102944",
  "language": "fr",
  "quality_score": null,
  "quality_updated_at": null,
  "waba_id": null
}
DocsTemplates/templates/:id
GEThttps://api.envoisms.ma/v1/templates/:id
Bearer

Retrieve a template

Returns one active template of the account, with its status and, when rejected, rejection_reason. 404 NOT_FOUND otherwise.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
GEThttps://api.envoisms.ma/v1/templates/:id
curl -X GET "https://api.envoisms.ma/v1/templates/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
  "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
  "name": "order_shipped",
  "channel": "whatsapp",
  "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
  "variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
  "category": "utility",
  "active": 1,
  "created_at": "2026-10-20 14:02:11",
  "status": "approved",
  "rejection_reason": null,
  "meta_template_id": "1203948576102938",
  "language": "fr",
  "quality_score": "GREEN",
  "quality_updated_at": "2026-10-21 08:00:00",
  "waba_id": null
}
DocsTemplates/templates/:id
PATCHhttps://api.envoisms.ma/v1/templates/:id
Bearer

Update a template

Updates a template with the same fields as creation; only the fields you send change. Returns the updated row.

Request body (JSON)
FieldTypeRequirementDescription
name | body | category | language | sample_values | header_* | footer | buttons | carouselmixedOptionalSame fields and rules as POST /v1/templates. An SMS template accepts only name, body, category and variables.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • WhatsApp, template not yet on Meta: the edit sends it back to review (pending_admin, then the automatic checks). Template already on Meta: the edit is submitted to Meta and moves it back to pending_meta; name and language can no longer change (400 TEMPLATE_IMMUTABLE_FIELD), nor can an approved template’s category; no edit while pending_meta, in_appeal or disabled (409 TEMPLATE_EDIT_NOT_ALLOWED).
PATCHhttps://api.envoisms.ma/v1/templates/:id
curl -X PATCH "https://api.envoisms.ma/v1/templates/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
  "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
  "name": "order_shipped",
  "channel": "whatsapp",
  "body": "Bonjour {{1}}, votre commande {{2}} est en route.",
  "variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
  "category": "utility",
  "active": 1,
  "created_at": "2026-10-20 14:02:11",
  "status": "pending_meta",
  "rejection_reason": null,
  "meta_template_id": "1203948576102938",
  "language": "fr",
  "quality_score": "GREEN",
  "quality_updated_at": "2026-10-21 08:00:00",
  "waba_id": null
}
DocsTemplates/templates/:id
DELETEhttps://api.envoisms.ma/v1/templates/:id
Bearer

Delete a template

Deactivates the template. A WhatsApp template already on Meta is deleted there too; meta_deleted says whether Meta did, meta_note why not.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
DELETEhttps://api.envoisms.ma/v1/templates/:id
curl -X DELETE "https://api.envoisms.ma/v1/templates/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "deleted": true,
  "id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
  "meta_deleted": true
}
DocsTemplates/templates/:id/submit-meta
POSThttps://api.envoisms.ma/v1/templates/:id/submit-meta
Bearer

Submit a template to Meta

Submits to Meta a WhatsApp template that already passed EnvoiSMS review but is not on Meta (Meta deleted it, for example). It is not a way around review.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • Refusals: 409 TEMPLATE_PENDING_REVIEW (the template awaits EnvoiSMS review; it goes to Meta once approved), 409 TEMPLATE_REJECTED_BY_REVIEW (rejected by review; edit it to send it back to review), 409 TEMPLATE_ALREADY_ON_META, 400 INVALID_CHANNEL (not a WhatsApp template).
POSThttps://api.envoisms.ma/v1/templates/:id/submit-meta
curl -X POST "https://api.envoisms.ma/v1/templates/{id}/submit-meta" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "ok": true,
  "template": {
    "id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
    "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
    "name": "order_shipped",
    "channel": "whatsapp",
    "body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
    "variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
    "category": "utility",
    "active": 1,
    "created_at": "2026-10-20 14:02:11",
    "status": "pending_meta",
    "rejection_reason": null,
    "meta_template_id": "1203948576102951",
    "language": "fr",
    "quality_score": null,
    "quality_updated_at": null,
    "waba_id": null
  },
  "meta": { "id": "1203948576102951", "status": "PENDING", "category": "UTILITY" }
}
DocsTemplates/templates/sync
POSThttps://api.envoisms.ma/v1/templates/sync
Bearer

Sync templates from Meta

Imports the templates of your WhatsApp Business Accounts (status, category, quality). A template Meta no longer holds becomes deleted_on_meta, unless the sync is incomplete (complete: false).

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
POSThttps://api.envoisms.ma/v1/templates/sync
curl -X POST "https://api.envoisms.ma/v1/templates/sync" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "ok": true,
  "wabas": ["104857392017465"],
  "fetched": 12,
  "updated": 9,
  "inserted": 3,
  "deduped": 0,
  "marked_deleted": 1,
  "complete": true,
  "errors": []
}
DocsSender IDs/sender-ids
GEThttps://api.envoisms.ma/v1/sender-ids
Bearer

List Sender IDs

Returns your SMS Sender ID requests and their status, plus a name suggested from your profile.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: sender_id.
GEThttps://api.envoisms.ma/v1/sender-ids
curl -X GET "https://api.envoisms.ma/v1/sender-ids" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "sid_6a2c8e4b0d1f3a5c7e9b2d4f",
      "sender_id": "MABOUTIQUE",
      "status": "approved",
      "rc_url": "/v1/sender-ids/rc/acct_7b2e4c6a8d0f1e3b5c7a9d2f/rc_1f3a5c7e9b2d4f6a8c0e2b4d.pdf",
      "note": null,
      "requested_at": "2026-10-02 10:15:00",
      "reviewed_at": "2026-10-09 16:40:00",
      "company_website": "https://maboutique.ma",
      "sample_content": "Votre commande 4512 est expédiée.",
      "message_type": "transactional",
      "estimated_volume": "5000/mois",
      "operator_emailed_at": "2026-10-02 10:20:00"
    }
  ],
  "suggestedSenderId": "MABOUTIQUE"
}
DocsSender IDs/sender-ids
POSThttps://api.envoisms.ma/v1/sender-ids
Bearer

Request a Sender ID

Files an SMS Sender ID request with your commercial register as a PDF. multipart/form-data body. Returns 201 (200 when an existing request is updated) with status pending.

Request body (JSON)
FieldTypeRequirementDescription
sender_idstringRequiredRequested name: unaccented letters, digits and spaces, at most 11 characters, at least one letter. A generic header (INFO, ALERT, …) or a protected institution’s name is refused.
filefileRequiredCommercial register, a PDF of at most 5 MB.
message_typestringOptionaltransactional (default) or promotional.
company_website | sample_content | estimated_volumestringOptionalCompany website, sample message and estimated volume, passed to the operators with the request.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: sender_id.
  • Errors: 400 INVALID_FORM_DATA, MISSING_FIELD, INVALID_FIELD, SENDER_ID_GENERIC, INVALID_FILE_TYPE, FILE_TOO_LARGE; 403 SENDER_ID_PROTECTED_BRAND; 409 SENDER_ID_PENDING or SENDER_ID_APPROVED.
POSThttps://api.envoisms.ma/v1/sender-ids
curl -X POST "https://api.envoisms.ma/v1/sender-ids" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "sid_6a2c8e4b0d1f3a5c7e9b2d4f",
  "sender_id": "MABOUTIQUE",
  "status": "pending",
  "rc_url": "/v1/sender-ids/rc/acct_7b2e4c6a8d0f1e3b5c7a9d2f/rc_1f3a5c7e9b2d4f6a8c0e2b4d.pdf"
}
DocsWhatsApp/whatsapp/waba-connection
GEThttps://api.envoisms.ma/v1/whatsapp/waba-connection
Bearer

WhatsApp Business connection

Returns the state of your connected WhatsApp Business numbers and the default number’s checklist (readiness): what, according to Meta, stops it from sending. Read-only; no secret is returned.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Any valid API key: no specific permission is required.
  • readiness: can_send is AVAILABLE, LIMITED, BLOCKED or unknown (Meta did not answer); blockers lists Meta’s reasons (entity, reason, resolution); payment_method is missing when Meta reports no payment method, unchecked otherwise; business_verification is Meta’s business verification status. readiness is null without a connected number. What Meta did not say comes back null, never as a healthy default.
  • Connecting, disconnecting or reconfiguring a number, and every write under /v1/whatsapp (profile, settings, tools), happens from a dashboard session: an API key gets 403 FORBIDDEN. Only POST /v1/whatsapp/lookup-phone and POST /v1/whatsapp/media/upload stay open to keys.
GEThttps://api.envoisms.ma/v1/whatsapp/waba-connection
curl -X GET "https://api.envoisms.ma/v1/whatsapp/waba-connection" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "readiness": {
    "waba_id": "104857392017465",
    "payment_method": "unchecked",
    "can_send": "AVAILABLE",
    "blockers": [],
    "business_verification": "verified",
    "approved_templates": 4,
    "checked_at": "2026-10-22T08:00:12.000Z"
  },
  "connected": true,
  "mode": "custom_waba",
  "gateway_number": "+212 708-789095",
  "waba_id": "104857392017465",
  "phone_number_id": "109283746501928",
  "phone_number": "+212522000000",
  "name": "Ma Boutique",
  "status": "connected",
  "error": null,
  "quality_rating": "GREEN",
  "messaging_limit_tier": "TIER_1K",
  "connected_at": "2026-09-30 11:02:44",
  "meta_app_id": "1029384756102938",
  "meta_config_id": "2039485761029384",
  "preverified_phone_ids": [],
  "connections": [
    {
      "id": "5f1c9e3a7b2d4f6a8c0e1b3d5f7a9c2e",
      "waba_id": "104857392017465",
      "phone_number_id": "109283746501928",
      "phone_number": "+212522000000",
      "verified_name": "Ma Boutique",
      "status": "connected",
      "is_default": true,
      "webhook_url": null,
      "settings": {},
      "created_at": "2026-09-30 11:02:44",
      "updated_at": "2026-10-20 08:15:02"
    }
  ]
}
DocsWhatsApp/whatsapp/profile
GEThttps://api.envoisms.ma/v1/whatsapp/profile
Bearer

WhatsApp Business profile

Returns the public profile of one of your numbers as Meta holds it: about, address, description, email, picture, websites, vertical.

Query parameters
ParameterTypeRequirementDescription
phone_number_idstringOptionalConnected number to read; defaults to your default number.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Any valid API key: no specific permission is required.
  • The profile is updated from the dashboard; with an API key, POST /v1/whatsapp/profile returns 403 FORBIDDEN.
GEThttps://api.envoisms.ma/v1/whatsapp/profile
curl -X GET "https://api.envoisms.ma/v1/whatsapp/profile" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "ok": true,
  "phone_number_id": "109283746501928",
  "profile": {
    "about": "Service client",
    "address": "Casablanca, Maroc",
    "description": "Prêt-à-porter, livraison partout au Maroc.",
    "email": "contact@maboutique.ma",
    "profile_picture_url": "https://pps.whatsapp.net/v/t61/...",
    "websites": ["https://maboutique.ma"],
    "vertical": "APPAREL"
  }
}
DocsVerify/verify/send
POSThttps://api.envoisms.ma/v1/verify/send
Bearer

Send a verification code

Generates a one-time code, sends it and opens a verification session. You store no code: validate what the user types with POST /v1/verify/check. Returns 202.

Request body (JSON)
FieldTypeRequirementDescription
tostringRequiredRecipient in E.164 format.
channelstringOptional"sms" (default) or "whatsapp". On WhatsApp the code goes out as an authentication template — from your own number when you have connected one with an approved authentication template, otherwise from the EnvoiSMS number — and falls back to SMS if Meta refuses the send.
app_idstringOptionalVerify application (vra_…) created in the dashboard: it sets length, validity, attempts, channels and cascade, and overrides code_length and expiry.
code_lengthintegerOptionalCode length, 4 to 8 digits. Default: 6.
expiryintegerOptionalCode validity in seconds, 60 to 1800. Default: 600. On WhatsApp, capped at 600.
brandstringOptional(SMS) Name shown in the message, at most 32 characters. Default: EnvoiSMS. A generic name is replaced by ENVOISMS; a protected institution’s name is refused (403 SENDER_ID_PROTECTED_BRAND).
templatestringOptional(SMS) Custom text with {{code}} and {{brand}}.
otp_button_textstringOptional(WhatsApp) Copy button label, at most 25 characters.
otp_type | package_name | signature_hashstringOptional(WhatsApp) copy_code (default), one_tap, zero_tap or url; for one_tap and zero_tap, the Android package name and signature hash of the app that receives the code.
web_otp_domain | app_hashstringOptional(SMS) Domain for WebOTP autofill (appends "@domain #code" to the SMS); 11-character Android app hash for the SMS Retriever API.
auto_cascadebooleanOptionalAlso sends the SMS fallback when the first channel’s timeout expires without delivery. Overrides the Verify application’s setting.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: verify.
  • On WhatsApp the response carries "channel": "whatsapp", "resend_after_seconds": 60, "fallback_channels": ["sms", "whatsapp"] and "cost" ({eur, mad}), without message_id. With a sandbox key nothing is sent or debited and the response adds "sandbox": true, "simulated": true and "sandbox_code", the code to submit to /v1/verify/check.
  • Per-number limits: one send every 30 s on SMS and 60 s on WhatsApp (429 OTP_COOLDOWN, with Retry-After), 5 per hour and 10 per day (429 OTP_RATE_LIMIT); 200 sends per day per account.
  • Autofill: on your site, add autocomplete="one-time-code" and inputmode="numeric" to the code input field.
POSThttps://api.envoisms.ma/v1/verify/send
curl -X POST "https://api.envoisms.ma/v1/verify/send" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+212612345678",
    "brand": "MyApp"
  }'
{
  "session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
  "message_id": "msg_2b4d6f8a0c1e3b5d7f9a1c3e",
  "to": "+212612345678",
  "channel": "sms",
  "expires_at": "2026-10-22T09:24:03.000Z",
  "resend_after_seconds": 30,
  "fallback_channels": [],
  "status": "sent"
}
DocsVerify/verify/resend
POSThttps://api.envoisms.ma/v1/verify/resend
Bearer

Resend the code

Resends the code of a session that is still open, by SMS or WhatsApp, once the cooldown has passed. The code does not change. Returns 200.

Request body (JSON)
FieldTypeRequirementDescription
session_idstringRequiredSession returned by /v1/verify/send (vrf_…).
channelstringOptional"sms" (default) or "whatsapp".
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: verify.
  • The cooldown runs from the last actual send: 30 s after an SMS, 60 s after a WhatsApp message. GET /v1/verify/:id gives the countdown (resend_available_in_seconds) and the 429 OTP_COOLDOWN carries Retry-After. The window belongs to your account and the number.
  • Errors: 404 SESSION_NOT_FOUND (unknown, expired, or from the other key mode), 400 SESSION_EXPIRED, 400 INVALID_STATE (session already verified or closed), 400 UNSUPPORTED_CHANNEL, 429 OTP_COOLDOWN. With a sandbox key the response adds "sandbox": true, "simulated": true and "sandbox_code".
POSThttps://api.envoisms.ma/v1/verify/resend
curl -X POST "https://api.envoisms.ma/v1/verify/resend" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "vrf_…",
    "channel": "sms"
  }'
{
  "session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
  "message_id": "msg_8c0e2b4d6f1a3c5e7b9d1f3a",
  "to": "+212612345678",
  "channel": "sms",
  "status": "sent",
  "resend_after_seconds": 30,
  "expires_at": "2026-10-22T09:24:03.000Z"
}
DocsVerify/verify/check
POSThttps://api.envoisms.ma/v1/verify/check
Bearer

Check a code

Compares the code the user typed with the session’s code. A correct code closes the session: a second check returns 404.

Request body (JSON)
FieldTypeRequirementDescription
session_idstringRequiredSession returned by /v1/verify/send.
codestringRequiredThe code the user typed.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: status.
  • Errors: 400 INVALID_CODE (the message gives the attempts left), 429 MAX_ATTEMPTS (the session becomes failed), 404 SESSION_NOT_FOUND — including for an expired or already verified session. /v1/verify never returns EXPIRED_CODE.
POSThttps://api.envoisms.ma/v1/verify/check
curl -X POST "https://api.envoisms.ma/v1/verify/check" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "vrf_…",
    "code": "483921"
  }'
{
  "session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
  "verified": true,
  "verified_at": "2026-10-22T09:15:12.408Z"
}
DocsVerify/verify/void
POSThttps://api.envoisms.ma/v1/verify/void
Bearer

Void a session

Closes a pending session: the code sent is no longer accepted. Also available as POST /v1/verify/:id/void.

Request body (JSON)
FieldTypeRequirementDescription
session_idstringRequiredSession to void (omit when the id is in the path).
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: status.
  • Errors: 404 SESSION_NOT_FOUND, 409 INVALID_STATE (session already verified, failed or expired). When the Verify application has a callback_url, it is notified (expired).
POSThttps://api.envoisms.ma/v1/verify/void
curl -X POST "https://api.envoisms.ma/v1/verify/void" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "vrf_…"
  }'
{
  "session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
  "status": "expired",
  "voided": true
}
DocsVerify/verify/:id
GEThttps://api.envoisms.ma/v1/verify/:id
Bearer

Retrieve a session

Returns a verification session’s state: status, attempts, countdown before resend and the delivery outcome of each send.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: status.
  • status is pending, verified, failed or expired. 404 NOT_FOUND when the session does not belong to the account.
GEThttps://api.envoisms.ma/v1/verify/:id
curl -X GET "https://api.envoisms.ma/v1/verify/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
  "session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
  "to": "+212612345678",
  "channel": "sms",
  "status": "pending",
  "attempts_used": 1,
  "attempts_remaining": 2,
  "max_attempts": 3,
  "resend_available_in_seconds": 0,
  "can_resend_now": true,
  "expires_at": "2026-10-22T09:24:03.000Z",
  "verified_at": null,
  "created_at": "2026-10-22 09:14:03",
  "app_id": null,
  "app_name": null,
  "attempted_channels": [],
  "deliveries": [
    {
      "channel": "sms",
      "kind": "send",
      "status": "delivered",
      "simulated": false,
      "submitted_at": "2026-10-22 09:14:03",
      "delivered_at": "2026-10-22 09:14:07",
      "read_at": null,
      "failed_at": null,
      "error_code": null,
      "error_message": null
    }
  ],
  "callback_delivered": null,
  "cascade_triggered": false
}
DocsVerify/lookup/:number
GEThttps://api.envoisms.ma/v1/lookup/:number
Bearer

Look up a number’s operator

Validates a number and returns its country and, for a Moroccan mobile, the operator that serves it, including ported numbers. Billed per successful lookup; an invalid number (400) is not billed.

Query parameters
ParameterTypeRequirementDescription
numberstringOptionalAlternative to the path parameter: GET /v1/lookup?number=%2B212612345678.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • Errors: 400 MISSING_FIELD, 400 INVALID_PHONE, 402 INSUFFICIENT_BALANCE. With a sandbox key the answer is real but nothing is debited, and it carries "sandbox": true. type is mobile or unknown; operator is null when unknown.
GEThttps://api.envoisms.ma/v1/lookup/:number
curl -X GET "https://api.envoisms.ma/v1/lookup/{number}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "lkp_5d7f9b1c3e5a7c9e1b3d5f7a",
  "number": "+212612345678",
  "valid": true,
  "country_iso": "MA",
  "type": "mobile",
  "operator": "Maroc Telecom",
  "ported": false,
  "cost": { "eur": 0.005, "mad": 0.055 }
}
DocsVerify/hlr/:number
GEThttps://api.envoisms.ma/v1/hlr/:number
Bearer

Query the network (HLR)

Queries the operator’s Home Location Register: is the number reachable right now, which network serves it today (ported numbers included), is it roaming, mobile or fixed line. Billed per answer from the network, whatever it is; an invalid number (400) or a query that did not complete (502/503) is not billed.

Query parameters
ParameterTypeRequirementDescription
numberstringOptionalAlternative to the path parameter: GET /v1/hlr?number=%2B212612345678.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • status is active (reachable), absent (known but switched off or out of coverage), inactive (not in service), invalid or unknown. The answer takes one network round trip (1–5 s). source says where the answer came from: network = the operator’s register queried live (status reflects reachability); portability = the operators’ number-portability database (Moroccan numbers): operator, ported flag and line in service (active), without saying whether the handset is on right now. Errors: 400 MISSING_FIELD, 400 INVALID_PHONE, 402 INSUFFICIENT_BALANCE, 502 BAD_GATEWAY, 503 HLR_UNAVAILABLE. With a sandbox key the answer is simulated from what the platform already knows, nothing is queried or debited, and it carries "sandbox": true.
GEThttps://api.envoisms.ma/v1/hlr/:number
curl -X GET "https://api.envoisms.ma/v1/hlr/{number}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "hlr_5d7f9b1c3e5a7c9e1b3d5f7a",
  "number": "+212612345678",
  "valid": true,
  "status": "active",
  "type": "mobile",
  "country_iso": "MA",
  "operator": "Maroc Telecom",
  "mcc": "604",
  "mnc": "01",
  "ported": false,
  "ported_operator": null,
  "ported_mnc": null,
  "roaming": false,
  "roaming_country_iso": null,
  "roaming_operator": null,
  "roaming_mnc": null,
  "source": "portability",
  "cost": { "eur": 0.018, "mad": 0.20 }
}
DocsVerify/verify/lookup
POSThttps://api.envoisms.ma/v1/verify/lookup
Bearer

Validate a number’s format

Validates a number’s format and returns its local and international forms, country, location, allocating carrier and line type. Not billed.

Request body (JSON)
FieldTypeRequirementDescription
numberstringRequiredNumber, in local or international format.
country_codestringOptionalISO 2-letter country code (MA, FR, …), recommended for a number in local format.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: status.
  • carrier is the operator the number range was allocated to, not necessarily the one serving a ported number: for the serving operator, use GET /v1/lookup/:number. Errors: 400 MISSING_FIELD; 503 LOOKUP_UNAVAILABLE when the service is briefly unavailable.
POSThttps://api.envoisms.ma/v1/verify/lookup
curl -X POST "https://api.envoisms.ma/v1/verify/lookup" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "number": "+212612345678"
  }'
{
  "valid": true,
  "number": "212612345678",
  "local_format": "0612345678",
  "international_format": "+212612345678",
  "country_prefix": "+212",
  "country_code": "MA",
  "country_name": "Morocco",
  "location": null,
  "carrier": "Maroc Telecom",
  "line_type": "mobile"
}
DocsConversations/conversations
GEThttps://api.envoisms.ma/v1/conversations
Bearer

List conversations

Returns the account’s WhatsApp and SMS threads, with each one’s 24-hour window state and overall counters.

Query parameters
ParameterTypeRequirementDescription
limitintegerOptionalNumber of results: 50 by default, at most 100.
offsetintegerOptionalNumber of results to skip (pagination). Default: 0.
filterstringOptionalall (default), unread, active_window or needs_human.
channelstringOptionalall (default), whatsapp or sms.
phone_number_id | assigned_to | searchstringOptionalConnected WhatsApp number; assigned agent; search in the number, name and last message.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
GEThttps://api.envoisms.ma/v1/conversations
curl -X GET "https://api.envoisms.ma/v1/conversations?limit=50" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "th_212612345678",
      "phone": "212612345678",
      "contact_name": "Yassine Alami",
      "channel": "whatsapp",
      "lead_status": "new",
      "unread_count": 1,
      "bot_muted": false,
      "handling_agent": null,
      "notes": null,
      "tags": [],
      "last_inbound_at": "2026-10-22 08:42:00",
      "last_outbound_at": "2026-10-21 17:10:00",
      "last_message_preview": "Bonjour, la veste existe en taille L ?",
      "last_message_role": "customer",
      "last_message_at": "2026-10-22 08:42:00",
      "is_window_active": true,
      "window_expires_at": "2026-10-23T08:42:00.000Z",
      "window_seconds_remaining": 84960,
      "waba_name": "Ma Boutique",
      "waba_phone": "+212522000000",
      "phone_number_id": "109283746501928"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0,
  "stats": {
    "total_threads": 1,
    "unread_threads": 1,
    "active_window_threads": 1,
    "human_takeover_threads": 0
  }
}
DocsConversations/conversations/:phone
GEThttps://api.envoisms.ma/v1/conversations/:phone
Bearer

Retrieve a conversation

Returns one thread and its messages (at most 500), oldest first, with the 24-hour window state.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • :phone accepts the number with or without +. role is customer for a received message, agent or bot for a sent one.
GEThttps://api.envoisms.ma/v1/conversations/:phone
curl -X GET "https://api.envoisms.ma/v1/conversations/{phone}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "thread": {
    "id": "th_212612345678",
    "phone": "212612345678",
    "contact_name": "Yassine Alami",
    "channel": "whatsapp",
    "lead_status": "new",
    "unread_count": 0,
    "bot_muted": true,
    "handling_agent": "Salma",
    "notes": null,
    "tags": [],
    "last_inbound_at": "2026-10-22 08:42:00",
    "last_outbound_at": "2026-10-22 08:45:10",
    "is_window_active": true,
    "window_expires_at": "2026-10-23T08:42:00.000Z",
    "window_seconds_remaining": 84960,
    "window": {
      "is_window_active": true,
      "window_expires_at": "2026-10-23T08:42:00.000Z",
      "window_seconds_remaining": 84960
    },
    "waba_name": "Ma Boutique",
    "waba_phone": "+212522000000",
    "phone_number_id": "109283746501928"
  },
  "messages": [
    {
      "id": "wamsg_1c3e5a7c9e1b3d5f7a9c1e3b",
      "phone": "212612345678",
      "role": "customer",
      "message": "Bonjour, la veste existe en taille L ?",
      "wamid": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg",
      "status": "read",
      "media_type": "text",
      "media_url": null,
      "metadata": {},
      "read_at": "2026-10-22 08:43:00",
      "created_at": "2026-10-22 08:42:00"
    }
  ]
}
DocsConversations/conversations/:phone/messages
POSThttps://api.envoisms.ma/v1/conversations/:phone/messages
Bearer

Reply in a conversation

Sends an agent reply (text, media or template) and pauses the bot on that thread. The send goes through the same path as POST /v1/messages: pricing, the opt-out list and the debit apply. Returns 201.

Request body (JSON)
FieldTypeRequirementDescription
messagestringOptionalReply text, or the media caption. Required without a template or media (400 MISSING_MESSAGE).
channelstringOptional"whatsapp" (default) or "sms".
template_id | template_name | template_language | variablesmixedOptionalApproved WhatsApp template to send instead of free text (by id, or by name and language), with its values (array or object).
media_url | media_type | filenamestringOptionalAttached media: https URL (or data URI), type image or document, document file name.
phone_number_idstringOptionalConnected WhatsApp number that replies.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: send.
  • 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 go out at any time. Other errors: 403 WHATSAPP_NOT_CONNECTED, 404 TEMPLATE_NOT_FOUND, 400 TEMPLATE_NOT_APPROVED.
POSThttps://api.envoisms.ma/v1/conversations/:phone/messages
curl -X POST "https://api.envoisms.ma/v1/conversations/{phone}/messages" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Thanks, we will call you back today."
  }'
{
  "ok": true,
  "message": {
    "id": "msg_9e1b3d5f7a9c1e3b5d7f9a1c",
    "phone": "212612345678",
    "role": "agent",
    "message": "Oui, la taille L est disponible.",
    "wamid": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAERgS",
    "status": "sent",
    "channel": "whatsapp",
    "media_type": "text",
    "metadata": { "agent": "Salma" },
    "created_at": "2026-10-22T08:45:10.221Z"
  }
}
DocsLeads/qualified-leads
GEThttps://api.envoisms.ma/v1/qualified-leads
Bearer

List qualified leads

Returns the leads your WhatsApp bot recorded, newest first, with their score (0 to 100) and status. format=csv downloads the same list.

Query parameters
ParameterTypeRequirementDescription
limitintegerOptionalNumber of results: 100 by default, at most 500.
statusstringOptionalnew, contacted, qualified, Fournisseur, won or lost.
presetstringOptionalBot sector (for example medical_equipment); supplier groups supplier enquiries.
formatstringOptionaljson (default) or csv.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Any valid API key: no specific permission is required.
  • No offset pagination: limit bounds the list. metrics is computed over the returned leads; hot counts those scoring 80 or more.
GEThttps://api.envoisms.ma/v1/qualified-leads
curl -X GET "https://api.envoisms.ma/v1/qualified-leads?limit=50" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "ok": true,
  "leads": [
    {
      "id": "lead_3a5c7e9b1d3f5a7c9e1b3d5f",
      "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
      "phone_number_id": "109283746501928",
      "contact_phone": "+212612345678",
      "contact_name": "Cabinet Benjelloun",
      "preset": "medical_equipment",
      "category": "Échographe",
      "bant_score": 85,
      "urgency": "immediate",
      "budget": "200 000 - 300 000 MAD",
      "authority": "decision_maker",
      "notes": null,
      "source_type": "direct_inbound",
      "status": "new",
      "created_at": "2026-10-21 09:15:00",
      "updated_at": "2026-10-21 09:15:00"
    }
  ],
  "metrics": { "total": 1, "hot": 1, "average_score": 85 }
}
DocsLeads/qualified-leads/:id
PATCHhttps://api.envoisms.ma/v1/qualified-leads/:id
Bearer

Update a lead

Changes a lead’s status, notes or name. Returns the updated lead.

Request body (JSON)
FieldTypeRequirementDescription
statusstringOptionalnew, contacted, qualified, Fournisseur, won or lost (otherwise 400 INVALID_STATUS).
notesstringOptionalInternal follow-up notes.
contact_namestringOptionalContact or business name.
value_minor | currencyinteger | stringOptionalDeal value in minor units and its currency, passed to Meta with a move to won.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Any valid API key: no specific permission is required.
  • A move to qualified or won is reported to Meta (QualifiedLead, Purchase) when the lead came from a click-to-WhatsApp ad; only on a real status change. 404 LEAD_NOT_FOUND when the lead does not belong to the account.
PATCHhttps://api.envoisms.ma/v1/qualified-leads/:id
curl -X PATCH "https://api.envoisms.ma/v1/qualified-leads/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "contacted",
    "notes": "Called back on 22/10"
  }'
{
  "ok": true,
  "lead": {
    "id": "lead_3a5c7e9b1d3f5a7c9e1b3d5f",
    "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
    "phone_number_id": "109283746501928",
    "contact_phone": "+212612345678",
    "contact_name": "Cabinet Benjelloun",
    "preset": "medical_equipment",
    "category": "Échographe",
    "bant_score": 85,
    "urgency": "immediate",
    "budget": "200 000 - 300 000 MAD",
    "authority": "decision_maker",
    "notes": "Rendez-vous fixé jeudi.",
    "source_type": "direct_inbound",
    "status": "contacted",
    "created_at": "2026-10-21 09:15:00",
    "updated_at": "2026-10-22 10:02:31",
    "user_id": null
  }
}
DocsContacts/contacts
GEThttps://api.envoisms.ma/v1/contacts
Bearer

List contacts

Returns the account’s contacts, with search and a list filter.

Query parameters
ParameterTypeRequirementDescription
limitintegerOptionalNumber of results: 100 by default, at most 500.
offsetintegerOptionalNumber of results to skip (pagination). Default: 0.
qstringOptionalSearch in name, phone and email.
list_idstringOptionalOnly members of this list (lst_…).
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
GEThttps://api.envoisms.ma/v1/contacts
curl -X GET "https://api.envoisms.ma/v1/contacts?limit=50" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b",
      "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
      "phone": "+212612345678",
      "name": "Karim Bennani",
      "email": "karim@example.ma",
      "custom1": "VIP",
      "custom2": null,
      "custom3": null,
      "opted_out": 0,
      "opted_out_at": null,
      "created_at": "2026-10-10 14:20:00"
    }
  ],
  "limit": 100,
  "offset": 0,
  "total": 1
}
DocsContacts/contacts
POSThttps://api.envoisms.ma/v1/contacts
Bearer

Create or update a contact

Creates a contact, or updates the one that already has this number (name, email and custom fields are replaced). Returns 201 with the contact.

Request body (JSON)
FieldTypeRequirementDescription
phonestringRequiredNumber in E.164 format (otherwise 400 INVALID_PHONE).
name | emailstringOptionalContact name and email.
list_idstringOptionalAlso adds the contact to this list (404 LIST_NOT_FOUND when it is not yours).
custom_fieldsobjectOptional{"custom1", "custom2", "custom3"}: three free fields, usable in campaigns.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
POSThttps://api.envoisms.ma/v1/contacts
curl -X POST "https://api.envoisms.ma/v1/contacts" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+212612345678",
    "name": "Karim Bennani",
    "email": "karim@example.ma"
  }'
{
  "id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b",
  "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
  "phone": "+212612345678",
  "name": "Karim Bennani",
  "email": "karim@example.ma",
  "custom1": "VIP",
  "custom2": null,
  "custom3": null,
  "opted_out": 0,
  "opted_out_at": null,
  "created_at": "2026-10-10 14:20:00"
}
DocsContacts/contacts/import
POSThttps://api.envoisms.ma/v1/contacts/import
Bearer

Import contacts

Queues a contact import, as JSON or as a CSV file, and returns 202 at once; the import then runs in batches of 100.

Request body (JSON)
FieldTypeRequirementDescription
contactsarrayRequired(application/json) Objects {"phone", "name"?, "email"?, "custom1"?, "custom2"?, "custom3"?}. For a CSV, send text/csv or multipart/form-data (field file) and pass the list as ?list_id=.
list_idstringOptionalList that receives the imported contacts.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
  • The response is given at queue time: total counts the rows received, and imported repeats it as is. Invalid numbers are skipped during the import; check the result with GET /v1/contacts. Other content types: 415 UNSUPPORTED_MEDIA_TYPE.
POSThttps://api.envoisms.ma/v1/contacts/import
curl -X POST "https://api.envoisms.ma/v1/contacts/import" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "phone": "+212611111111",
        "name": "Karim"
      },
      {
        "phone": "+212622222222",
        "name": "Salma"
      }
    ]
  }'
{
  "jobId": "job_4b6d8f0a2c4e6b8d0f2a4c6e",
  "total": 150,
  "imported": 150,
  "skipped": 0,
  "status": "queued"
}
DocsContacts/contacts/:id
DELETEhttps://api.envoisms.ma/v1/contacts/:id
Bearer

Delete a contact

Permanently deletes a contact. 404 NOT_FOUND when it does not exist.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
DELETEhttps://api.envoisms.ma/v1/contacts/:id
curl -X DELETE "https://api.envoisms.ma/v1/contacts/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "deleted": true,
  "id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b"
}
DocsContacts/contacts/lists
GEThttps://api.envoisms.ma/v1/contacts/lists
Bearer

List contact lists

Returns the account’s contact lists, with their member count.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
GEThttps://api.envoisms.ma/v1/contacts/lists
curl -X GET "https://api.envoisms.ma/v1/contacts/lists" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
      "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
      "name": "Clients fidèles",
      "description": "Clients ayant commandé au moins deux fois",
      "count": 1420,
      "created_at": "2026-09-15 09:00:00"
    }
  ]
}
DocsContacts/contacts/lists
POSThttps://api.envoisms.ma/v1/contacts/lists
Bearer

Create a list

Creates an empty contact list, filled through POST /v1/contacts or /v1/contacts/import. Returns 201.

Request body (JSON)
FieldTypeRequirementDescription
namestringRequiredList name.
descriptionstringOptionalFree description.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
POSThttps://api.envoisms.ma/v1/contacts/lists
curl -X POST "https://api.envoisms.ma/v1/contacts/lists" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Newsletter",
    "description": "Customers who subscribed at checkout"
  }'
{
  "id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
  "name": "Clients fidèles",
  "description": "Clients ayant commandé au moins deux fois",
  "count": 0
}
DocsContacts/contacts/lists/:id
POSThttps://api.envoisms.ma/v1/contacts/lists/:id
Bearer

Rename a list

Replaces a list’s name and description. 404 NOT_FOUND when it does not exist.

Request body (JSON)
FieldTypeRequirementDescription
namestringRequiredNew name.
descriptionstringOptionalNew description; when omitted, it is cleared.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
POSThttps://api.envoisms.ma/v1/contacts/lists/:id
curl -X POST "https://api.envoisms.ma/v1/contacts/lists/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
  "name": "Clients fidèles 2026",
  "description": null
}
DocsContacts/contacts/lists/:id
DELETEhttps://api.envoisms.ma/v1/contacts/lists/:id
Bearer

Delete a list

Deletes a list and its memberships; the contacts themselves are kept. Refused (400 LIST_IN_USE) while a scheduled, running or paused campaign uses it.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
DELETEhttps://api.envoisms.ma/v1/contacts/lists/:id
curl -X DELETE "https://api.envoisms.ma/v1/contacts/lists/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "deleted": true,
  "id": "lst_f84b2d6a8c0e1f3b5d7a9c2e"
}
DocsContacts/optouts
GEThttps://api.envoisms.ma/v1/optouts
Bearer

List opt-outs

Returns the numbers opted out of your sends. No message goes to them (403 OPTED_OUT).

Query parameters
ParameterTypeRequirementDescription
limitintegerOptionalNumber of results: 100 by default, at most 500.
offsetintegerOptionalNumber of results to skip (pagination). Default: 0.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: optouts.
GEThttps://api.envoisms.ma/v1/optouts
curl -X GET "https://api.envoisms.ma/v1/optouts?limit=50" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    { "phone": "+212611111111", "opted_out_at": "2026-10-01 12:00:00" }
  ],
  "limit": 100,
  "offset": 0,
  "total": 1
}
DocsContacts/optouts
POSThttps://api.envoisms.ma/v1/optouts
Bearer

Add an opt-out

Adds a number to your opt-out list. Returns 201.

Request body (JSON)
FieldTypeRequirementDescription
phonestringRequiredNumber in E.164 format.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: optouts.
POSThttps://api.envoisms.ma/v1/optouts
curl -X POST "https://api.envoisms.ma/v1/optouts" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+212612345678"
  }'
{
  "phone": "+212611111111",
  "opted_out": true
}
DocsContacts/optouts/:phone
DELETEhttps://api.envoisms.ma/v1/optouts/:phone
Bearer

Remove an opt-out

Removes a number from the opt-out list. Do it only on the person’s fresh agreement, to be recorded with POST /v1/consents.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: optouts.
DELETEhttps://api.envoisms.ma/v1/optouts/:phone
curl -X DELETE "https://api.envoisms.ma/v1/optouts/{phone}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "phone": "+212611111111",
  "opted_out": false
}
DocsContacts/consents
GEThttps://api.envoisms.ma/v1/consents
Bearer

List consent events

Append-only consent ledger (Law 09-08): every grant and every withdrawal is a dated event, with its source and evidence. The platform itself records the STOP/START keywords it receives and the first conversation a customer opens.

Query parameters
ParameterTypeRequirementDescription
limitintegerOptionalNumber of results: 100 by default, at most 500.
offsetintegerOptionalNumber of results to skip (pagination). Default: 0.
phonestringOptionalE.164 number.
purposestringOptionalmarketing, transactional, otp or service.
statusstringOptionalgranted or withdrawn.
from | tostringOptionalISO 8601 bounds on recorded_at.
formatstringOptionalcsv: downloads the whole filtered set.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
GEThttps://api.envoisms.ma/v1/consents
curl -X GET "https://api.envoisms.ma/v1/consents?limit=50" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "cns_9f2a4c6e8b0d1f3a5c7e9b2d",
      "phone": "+212612345678",
      "channel": "whatsapp",
      "purpose": "marketing",
      "status": "withdrawn",
      "source": "whatsapp_keyword",
      "evidence": { "keyword": "stop", "lang": "fr" },
      "recorded_at": "2026-10-05T09:00:00.000Z",
      "created_at": "2026-10-05T09:00:00.000Z"
    }
  ],
  "limit": 100,
  "offset": 0,
  "total": 1
}
DocsContacts/consents
POSThttps://api.envoisms.ma/v1/consents
Bearer

Record a consent event

Records that a person granted or withdrew consent, with the evidence you hold. A marketing withdrawal puts the number on the opt-out list at once. Returns 201.

Request body (JSON)
FieldTypeRequirementDescription
phonestringRequiredNumber in E.164 format.
purposestringRequiredmarketing, transactional, otp or service.
statusstringOptionalgranted (default) or withdrawn.
channelstringOptionalwhatsapp, sms or any (default).
sourcestringOptionalapi (default), form, import or dashboard. Keyword sources are reserved to the platform.
evidenceobjectOptionalFree JSON object under 4,096 bytes: wording shown, URL, IP, reference.
recorded_atstringOptionalISO 8601 time of the act (default: now); may be backdated for an import, never in the future.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
  • Errors: 400 INVALID_PHONE, INVALID_PURPOSE, INVALID_STATUS, INVALID_CHANNEL, INVALID_SOURCE, INVALID_EVIDENCE, INVALID_DATE.
POSThttps://api.envoisms.ma/v1/consents
curl -X POST "https://api.envoisms.ma/v1/consents" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+212612345678",
    "purpose": "marketing",
    "status": "granted",
    "channel": "whatsapp",
    "source": "form",
    "evidence": {
      "form": "checkout",
      "ip": "196.200.1.4"
    }
  }'
{
  "id": "cns_1b3d5f7a9c2e4b6d8f0a2c4e",
  "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-10-01T10:00:00.000Z",
  "created_at": "2026-10-01T10:00:00.000Z"
}
DocsContacts/consents/:phone
GEThttps://api.envoisms.ma/v1/consents/:phone
Bearer

Consent state and history of a number

Returns the current state per purpose (from the newest event; unknown without history, never assumed granted), opt-out list membership and the history (at most 500 events).

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: contacts.
GEThttps://api.envoisms.ma/v1/consents/:phone
curl -X GET "https://api.envoisms.ma/v1/consents/{phone}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "phone": "+212612345678",
  "opted_out": true,
  "opted_out_at": "2026-10-05T09:00:00.000Z",
  "state": {
    "marketing": { "status": "withdrawn", "channel": "whatsapp", "source": "whatsapp_keyword", "recorded_at": "2026-10-05T09:00:00.000Z", "record_id": "cns_9f2a4c6e8b0d1f3a5c7e9b2d" },
    "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_0a2c4e6b8d1f3a5c7e9b0d2f" }
  },
  "history": []
}
DocsCampaigns/campaigns
GEThttps://api.envoisms.ma/v1/campaigns
Bearer

List campaigns

Returns the account’s campaigns, newest first, with their counters.

Query parameters
ParameterTypeRequirementDescription
limitintegerOptionalNumber of results: 50 by default, at most 200.
offsetintegerOptionalNumber of results to skip (pagination). Default: 0.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: campaigns.
  • Statuses: draft, scheduled, running, paused, completed, failed, cancelled.
GEThttps://api.envoisms.ma/v1/campaigns
curl -X GET "https://api.envoisms.ma/v1/campaigns?limit=50" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
      "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
      "name": "Soldes d’automne",
      "channel": "sms",
      "list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
      "template_id": null,
      "body": "Bonjour {{name}}, -30% sur la collection avec le code AUT30.",
      "sender_id": "MABOUTIQUE",
      "status": "completed",
      "scheduled_at": null,
      "started_at": "2026-10-15 10:00:00",
      "completed_at": "2026-10-15 10:06:12",
      "total_count": 1420,
      "sent_count": 1412,
      "delivered_count": 1380,
      "failed_count": 8,
      "cost_eur": 102.24,
      "cost_mad": 1124.64,
      "created_at": "2026-10-14 16:30:00",
      "buttons": null,
      "template_variables": null
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
DocsCampaigns/campaigns
POSThttps://api.envoisms.ma/v1/campaigns
Bearer

Create a campaign

Creates a campaign as a draft, or scheduled when scheduled_at is given. Nothing goes out before POST /v1/campaigns/:id/send. Returns 201.

Request body (JSON)
FieldTypeRequirementDescription
namestringRequiredCampaign name.
bodystringOptional(SMS) Message text. body or template_id is required.
template_idstringOptionalTemplate of the account. Required on WhatsApp, where Meta must have approved it (400 TEMPLATE_NOT_APPROVED).
channelstringOptional"sms" (default) or "whatsapp" (requires a connected WhatsApp Business number, otherwise 403 WHATSAPP_NOT_CONNECTED).
list_idstringOptionalRecipient list; needed to launch the campaign (404 LIST_NOT_FOUND when it is not yours).
sender_idstringOptionalSMS: Sender ID (same rules as POST /v1/messages; default EnvoiSMS). WhatsApp: connected number that sends (default: your default number; 400 INVALID_WABA_SENDER when it is not yours).
scheduled_atstringOptionalISO 8601 date; sets the campaign to scheduled.
template_variablesobjectOptional(WhatsApp) Value of each template parameter, by key. Key "1", "2", … or a variable name: a contact field (name, phone, email, custom1, custom2, custom3) or fixed text prefixed "static:". "_header_media_url": https link of the header media (required for an image, video or document header). "_button_<i>": the variable part of the URL button at index i (from 0). Without a value, {{1}} reads name, {{2}} custom1 and {{3}} custom2.
consent_attestedbooleanOptional(WhatsApp, marketing template) true: you attest that every contact on the list agreed to receive your WhatsApp messages. Required for a marketing template (otherwise 400 CONSENT_ATTESTATION_REQUIRED); the attestation is timestamped.
buttonsarrayOptional(WhatsApp) Buttons, same format as POST /v1/messages. Refused on SMS (400 BUTTONS_NOT_SUPPORTED).
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: campaigns.
  • WhatsApp: everything the template needs per contact is checked at creation; what is missing is named in 400 MISSING_TEMPLATE_PARAMS. Other errors: 400 MISSING_TEMPLATE, 404 TEMPLATE_NOT_FOUND.
POSThttps://api.envoisms.ma/v1/campaigns
curl -X POST "https://api.envoisms.ma/v1/campaigns" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer sale",
    "channel": "sms",
    "body": "Summer sale: -20% until Sunday. Reply STOP to unsubscribe.",
    "list_id": "lst_…"
  }'
{
  "id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
  "status": "draft"
}
DocsCampaigns/campaigns/:id
GEThttps://api.envoisms.ma/v1/campaigns/:id
Bearer

Retrieve a campaign

Returns one campaign, its status and its send counters. 404 NOT_FOUND otherwise.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: campaigns.
GEThttps://api.envoisms.ma/v1/campaigns/:id
curl -X GET "https://api.envoisms.ma/v1/campaigns/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
  "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
  "name": "Nouvelle collection",
  "channel": "whatsapp",
  "list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
  "template_id": "tpl_8e2a4c6b1d3f5a7c9e0b2d4f",
  "body": null,
  "sender_id": "+212522000000",
  "status": "running",
  "scheduled_at": null,
  "started_at": "2026-10-22 10:00:00",
  "completed_at": null,
  "total_count": 1420,
  "sent_count": 840,
  "delivered_count": 790,
  "failed_count": 3,
  "cost_eur": 0,
  "cost_mad": 0,
  "created_at": "2026-10-21 18:12:40",
  "buttons": null,
  "template_variables": "{\"1\":\"name\"}"
}
DocsCampaigns/campaigns/:id
PATCHhttps://api.envoisms.ma/v1/campaigns/:id
Bearer

Update a campaign

Updates a draft or scheduled campaign; only the fields you send change. Returns the updated campaign.

Request body (JSON)
FieldTypeRequirementDescription
name | body | sender_id | scheduled_atstringOptionalNew name, text, sender or scheduled date (ISO 8601). An empty value leaves the field unchanged.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: campaigns.
  • 409 CAMPAIGN_NOT_EDITABLE when the campaign is neither draft nor scheduled; 404 NOT_FOUND when it does not exist.
PATCHhttps://api.envoisms.ma/v1/campaigns/:id
curl -X PATCH "https://api.envoisms.ma/v1/campaigns/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduled_at": "2026-11-01T09:00:00Z"
  }'
{
  "id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
  "account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
  "name": "Soldes d’automne — relance",
  "channel": "sms",
  "list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
  "template_id": null,
  "body": "Dernier jour : -30% avec le code AUT30.",
  "sender_id": "MABOUTIQUE",
  "status": "scheduled",
  "scheduled_at": "2026-10-25T09:00:00Z",
  "started_at": null,
  "completed_at": null,
  "total_count": 0,
  "sent_count": 0,
  "delivered_count": 0,
  "failed_count": 0,
  "cost_eur": 0,
  "cost_mad": 0,
  "created_at": "2026-10-22 11:05:00",
  "buttons": null,
  "template_variables": null
}
DocsCampaigns/campaigns/:id
DELETEhttps://api.envoisms.ma/v1/campaigns/:id
Bearer

Cancel a campaign

Sets the campaign to cancelled; it can still be retrieved. A running campaign must be paused first (409 CAMPAIGN_RUNNING).

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: campaigns.
DELETEhttps://api.envoisms.ma/v1/campaigns/:id
curl -X DELETE "https://api.envoisms.ma/v1/campaigns/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "deleted": true,
  "id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}
DocsCampaigns/campaigns/:id/send
POSThttps://api.envoisms.ma/v1/campaigns/:id/send
Bearer

Launch a campaign

Launches the campaign to the members of its list who have not opted out. Sending runs in the background; follow it with GET /v1/campaigns/:id. Returns 202.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: campaigns.
  • Errors: 400 ALREADY_RUNNING, 400 MISSING_LIST, 400 EMPTY_LIST (no eligible recipient), 400 MISSING_BODY, 404 NOT_FOUND. Each message is priced and debited like a single send.
POSThttps://api.envoisms.ma/v1/campaigns/:id/send
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/send" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
  "status": "running",
  "total": 1420
}
DocsCampaigns/campaigns/:id/pause
POSThttps://api.envoisms.ma/v1/campaigns/:id/pause
Bearer

Pause a campaign

Pauses a running campaign. 409 NOT_RUNNING when it is not running.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: campaigns.
POSThttps://api.envoisms.ma/v1/campaigns/:id/pause
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/pause" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "paused": true,
  "id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}
DocsCampaigns/campaigns/:id/resume
POSThttps://api.envoisms.ma/v1/campaigns/:id/resume
Bearer

Resume a campaign

Resumes a paused campaign where it stopped. 409 NOT_PAUSED when it is not paused.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: campaigns.
POSThttps://api.envoisms.ma/v1/campaigns/:id/resume
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/resume" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "resumed": true,
  "id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}
DocsWebhooks/webhooks
GEThttps://api.envoisms.ma/v1/webhooks
Bearer

List webhooks

Returns your active webhooks. The secret appears only masked (secret_hint); it is shown in full only at creation and rotation.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: webhooks.
GEThttps://api.envoisms.ma/v1/webhooks
curl -X GET "https://api.envoisms.ma/v1/webhooks" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
      "name": "Webhook Endpoint",
      "target_type": "custom",
      "format": "standard",
      "url": "https://example.ma/hooks/envoisms",
      "events": "[\"message.delivered\",\"message.failed\"]",
      "active": 1,
      "last_triggered_at": "2026-10-22 09:14:09",
      "last_status": 200,
      "created_at": "2026-10-01 10:00:00",
      "secret_hint": "••••9c2e"
    }
  ]
}
DocsWebhooks/webhooks
POSThttps://api.envoisms.ma/v1/webhooks
Bearer

Create a webhook

Registers an HTTPS URL to receive the chosen events. Returns 201 with the signing secret, shown this one time.

Request body (JSON)
FieldTypeRequirementDescription
urlstringRequiredPublic HTTPS URL (otherwise 400 INVALID_URL).
eventsstring[]Optionalmessage.sent, message.delivered, message.read, message.failed, message.undeliverable, message.fallback, message.inbound, message.reaction, contact.optout, contact.optin, lead.qualified; wildcards "message.*" and "*". Default: ["message.delivered", "message.failed"].
secretstringOptionalSigning secret; generated (sec_…) when omitted.
name | formatstringOptionalLabel; format standard ({"event", "data", "timestamp"}) or flat (data fields also at the top level, for a spreadsheet).
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: webhooks.
  • Signature: every delivery carries X-EnvoiSMS-Signature-256: t=<unix time>,v1=<hex HMAC-SHA256 of "t.body" with the secret>, and X-EnvoiSMS-Event. Reject a timestamp older than 5 minutes. A failure (5xx, 429 or network) is retried after 60 s, 10 min, then 1 h: 4 attempts in all.
  • The number of active webhooks depends on the plan (Starter 2, Business 5, Pro 15, Enterprise 50); beyond it, 403 LIMIT_EXCEEDED.
POSThttps://api.envoisms.ma/v1/webhooks
curl -X POST "https://api.envoisms.ma/v1/webhooks" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.ma/webhooks/envoisms",
    "events": [
      "message.delivered",
      "message.failed",
      "message.inbound"
    ]
  }'
{
  "id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
  "name": "Webhook Endpoint",
  "target_type": "custom",
  "format": "standard",
  "url": "https://example.ma/hooks/envoisms",
  "events": ["message.delivered", "message.failed"],
  "secret": "sec_3d5f7a9c1e3b5d7f9a1c9c2e",
  "active": true
}
DocsWebhooks/webhooks/:id
PATCHhttps://api.envoisms.ma/v1/webhooks/:id
Bearer

Update a webhook

Updates a webhook’s URL, events, name, format or active state; only the fields you send change.

Request body (JSON)
FieldTypeRequirementDescription
url | events | name | formatmixedOptionalSame rules as at creation.
activebooleanOptionalfalse stops deliveries to this webhook.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: webhooks.
PATCHhttps://api.envoisms.ma/v1/webhooks/:id
curl -X PATCH "https://api.envoisms.ma/v1/webhooks/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "active": false
  }'
{
  "updated": true,
  "id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e"
}
DocsWebhooks/webhooks/:id
DELETEhttps://api.envoisms.ma/v1/webhooks/:id
Bearer

Delete a webhook

Deactivates the webhook: no further event is sent to it and it leaves the list.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: webhooks.
DELETEhttps://api.envoisms.ma/v1/webhooks/:id
curl -X DELETE "https://api.envoisms.ma/v1/webhooks/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "deleted": true,
  "id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e"
}
DocsWebhooks/webhooks/:id/test
POSThttps://api.envoisms.ma/v1/webhooks/:id/test
Bearer

Test a webhook

Sends a sample event, signed like a real one, to the webhook’s URL. Returns 202 with the sample sent.

Request body (JSON)
FieldTypeRequirementDescription
eventstringOptionalEvent name to simulate (default: lead.qualified). The sample data is an inbound message for message.inbound, a qualified lead otherwise.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: webhooks.
POSThttps://api.envoisms.ma/v1/webhooks/:id/test
curl -X POST "https://api.envoisms.ma/v1/webhooks/{id}/test" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "message.delivered"
  }'
{
  "queued": true,
  "id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
  "event": "message.inbound",
  "sample": {
    "event": "message.inbound",
    "data": {
      "message_id": "msg_demo_k3x9f2ab",
      "from": "+212612345678",
      "to": "+212708789095",
      "channel": "whatsapp",
      "text": "Bonjour, je souhaite des informations sur vos tarifs et solutions.",
      "contact_name": "Karim Bennani",
      "timestamp": "2026-10-22T09:30:00.000Z"
    },
    "timestamp": "2026-10-22T09:30:00.000Z"
  }
}
DocsWebhooks/webhooks/:id/rotate-secret
POSThttps://api.envoisms.ma/v1/webhooks/:id/rotate-secret
Bearer

Rotate a webhook secret

Replaces the signing secret and returns the new one, this one time. Deliveries already queued stay signed with the old one: accept both for a few minutes.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: webhooks.
POSThttps://api.envoisms.ma/v1/webhooks/:id/rotate-secret
curl -X POST "https://api.envoisms.ma/v1/webhooks/{id}/rotate-secret" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
  "secret": "sec_7a9c1e3b5d7f9a1c3e5b0d2f",
  "rotated": true
}
DocsWebhooks/webhooks/logs
GEThttps://api.envoisms.ma/v1/webhooks/logs
Bearer

Webhook delivery log

Returns the last 100 delivery attempts, newest first, with the body sent and your server’s response.

Query parameters
ParameterTypeRequirementDescription
webhook_idstringOptionalOnly deliveries of this webhook.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: webhooks.
GEThttps://api.envoisms.ma/v1/webhooks/logs
curl -X GET "https://api.envoisms.ma/v1/webhooks/logs" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "0b6f3c2e-8a4d-4f1b-9c7e-2d5a1f8e6b30",
      "webhook_id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
      "event": "message.delivered",
      "url": "https://example.ma/hooks/envoisms",
      "payload": "{\"event\":\"message.delivered\",\"data\":{\"id\":\"msg_3f9a1c7e5b2d4a6f8e0c1b3d\",\"to\":\"+212612345678\",\"channel\":\"sms\",\"status\":\"delivered\"},\"timestamp\":\"2026-10-22T09:14:09.120Z\"}",
      "status_code": 500,
      "error_message": null,
      "attempt": 1,
      "delivered": 0,
      "created_at": "2026-10-22 09:14:09"
    }
  ],
  "max_attempts": 4
}
DocsWebhooks/webhooks/logs/:log_id/replay
POSThttps://api.envoisms.ma/v1/webhooks/logs/:log_id/replay
Bearer

Replay a webhook delivery

Resends a logged delivery — same event, same data, a fresh signature — to the webhook’s current URL, with the usual retries. Returns 202.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: webhooks.
  • 404 NOT_FOUND when the log is unknown or its webhook was deleted; 422 REPLAY_UNAVAILABLE when the body sent was not kept.
POSThttps://api.envoisms.ma/v1/webhooks/logs/:log_id/replay
curl -X POST "https://api.envoisms.ma/v1/webhooks/logs/{log_id}/replay" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "queued": true,
  "replay_of": "0b6f3c2e-8a4d-4f1b-9c7e-2d5a1f8e6b30",
  "webhook_id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
  "event": "message.delivered"
}
DocsWebhooks/inbound-webhooks
GEThttps://api.envoisms.ma/v1/inbound-webhooks
Bearer

List inbound webhooks

Returns your inbound webhooks: catch URLs that turn a call from a third-party tool (form, Zapier, Make, …) into a sent message.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Any valid API key: no specific permission is required.
GEThttps://api.envoisms.ma/v1/inbound-webhooks
curl -X GET "https://api.envoisms.ma/v1/inbound-webhooks" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "inwh_9a8b7c6d5e4f3a2b1c0d9e8f",
      "name": "Formulaire de contact",
      "channel": "whatsapp",
      "template_id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
      "active": 1,
      "created_at": "2026-10-12 14:00:00"
    }
  ]
}
DocsWebhooks/inbound-webhooks
POSThttps://api.envoisms.ma/v1/inbound-webhooks
Bearer

Create an inbound webhook

Creates a catch URL, POST /v1/inbound-webhooks/catch/:id: each JSON call it receives sends a message to the number read from the body. Returns 201.

Request body (JSON)
FieldTypeRequirementDescription
namestringRequiredSource label.
channelstringRequiredsms or whatsapp.
mapping_configobjectRequired{"to_path": "contact.phone", "variables": {"1": "contact.name"}}: path of the number in the received JSON, and path of each template variable.
template_idstringOptionalTemplate to send (id or name).
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Any valid API key: no specific permission is required.
  • The catch URL takes no key: its id acts as the secret, so do not publish it. It accepts at most 64 KB and 120 calls per minute, returns 202 {"success": true, "messageId"} and debits your balance like a normal send. Delete with DELETE /v1/inbound-webhooks/:id.
POSThttps://api.envoisms.ma/v1/inbound-webhooks
curl -X POST "https://api.envoisms.ma/v1/inbound-webhooks" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Website form",
    "channel": "whatsapp",
    "mapping_config": {
      "to_path": "phone"
    }
  }'
{
  "id": "inwh_9a8b7c6d5e4f3a2b1c0d9e8f"
}
DocsBilling/billing/balance
GEThttps://api.envoisms.ma/v1/billing/balance
Bearer

Retrieve the balance

Returns the prepaid balance in dirhams and euros, the plan and its expiry date.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: balance.
  • GET /v1/balance returns the first four fields with the same permission.
GEThttps://api.envoisms.ma/v1/billing/balance
curl -X GET "https://api.envoisms.ma/v1/billing/balance" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "balance_mad": 1492.5,
  "balance_eur": 135.68,
  "primary_currency": "MAD",
  "plan": "business",
  "plan_expires_at": "2026-12-31 23:59:59",
  "plan_days_left": 70
}
DocsBilling/billing/transactions
GEThttps://api.envoisms.ma/v1/billing/transactions
Bearer

List transactions

Returns the balance movements, newest first: top-ups, usage, refunds, adjustments and grants, each with the balance after it.

Query parameters
ParameterTypeRequirementDescription
limitintegerOptionalNumber of results: 50 by default, at most 200.
offsetintegerOptionalNumber of results to skip (pagination). Default: 0.
typestringOptionaltopup, usage, refund, adjustment or grant (otherwise 400 INVALID_FIELD).
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: billing.
GEThttps://api.envoisms.ma/v1/billing/transactions
curl -X GET "https://api.envoisms.ma/v1/billing/transactions?limit=50" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "tx_6e8a0c2e4b6d8f0a2c4e6b8d",
      "type": "usage",
      "status": "completed",
      "amount_eur": -0.072,
      "amount_mad": -0.792,
      "balance_after_eur": 135.68,
      "balance_after_mad": 1492.5,
      "payment_method": null,
      "description": "SMS +212612345678",
      "reference": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
      "stripe_invoice_id": null,
      "created_at": "2026-10-22 09:14:03"
    }
  ],
  "limit": 50,
  "offset": 0,
  "total": 1,
  "type": null
}
DocsMachine Payments/machine-payments/status
GEThttps://api.envoisms.ma/v1/machine-payments/status
Public

Machine payments status

Says whether an agent without an account can pay for API access with a USDC deposit (HTTP 402, MPP protocol), and on what terms. No authentication.

Headers
Content-Type: application/json
Accept: application/json
Good to know
  • When disabled, the response is {"enabled": false}.
  • Flow: call /v1/messages, /v1/waba, /v1/verify or /v1/mcp with no Authorization header and with X-Payment-Method: mpp (or Accept: application/problem+json); the response is 402 with a WWW-Authenticate: Payment header describing the deposit. Deposit deposit_usd in USDC, then repeat the call with Authorization: Payment <credential>. The response carries the issued API key in X-Machine-Session-Key (once) and the receipt in Payment-Receipt; use that key as a Bearer key from then on.
  • Without X-Payment-Method or Accept: application/problem+json, an unauthenticated call gets 401 UNAUTHORIZED. A deposit not yet settled gives 402 PAYMENT_NOT_SETTLED; an unreadable credential, 400 INVALID_CREDENTIAL. The issued key holds only the send, status, balance and verify permissions. At most 5 challenges per minute per IP.
GEThttps://api.envoisms.ma/v1/machine-payments/status
curl -X GET "https://api.envoisms.ma/v1/machine-payments/status" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "enabled": true,
  "deposit_usd": 5,
  "credit_mad": 46.5,
  "networks": ["base", "tempo"],
  "permissions": ["send", "status", "balance", "verify"]
}
DocsAnalytics/analytics
GEThttps://api.envoisms.ma/v1/analytics
Bearer

Sending statistics

Returns, over a window of days, volumes, confirmed delivery, the spend debited and their breakdown by channel, operator, status and day.

Query parameters
ParameterTypeRequirementDescription
daysintegerOptionalWindow size, 30 by default, at most 365.
campaign_idstringOptionalStatistics of one campaign (the response then omits error_diagnostics and waba_breakdown).
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: analytics.
  • delivery_rate is computed over messages whose delivery was confirmed or denied only; coverage_pct says what share of messages got that confirmation. An unknown value is null, never 100. cost_* is what was debited, net of refunds. The response is cached for 60 seconds (X-Cache header).
GEThttps://api.envoisms.ma/v1/analytics
curl -X GET "https://api.envoisms.ma/v1/analytics" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "window_days": 30,
  "campaign_id": null,
  "summary": {
    "total": 420,
    "delivered": 398,
    "failed": 6,
    "unconfirmed": 16,
    "delivery_rate": 98.51,
    "coverage_pct": 96.19,
    "cost_eur": 30.24,
    "cost_mad": 332.64,
    "avg_latency": 4.2
  },
  "channels": [
    { "channel": "sms", "total": 420, "cost_eur": 30.24, "cost_mad": 332.64, "fallback_sends": 0 }
  ],
  "operators": [
    { "operator": "Maroc Telecom", "total": 250, "delivered": 240, "failed": 2, "unconfirmed": 8, "cost_eur": 18, "cost_mad": 198, "delivery_rate": 99.17, "coverage_pct": 96.8, "avg_latency": null }
  ],
  "status_distribution": [
    { "status": "delivered", "total": 398 },
    { "status": "unconfirmed", "total": 16 },
    { "status": "failed", "total": 6 }
  ],
  "daily": [
    { "day": "2026-10-21", "total": 14, "sms_total": 14, "whatsapp_total": 0, "delivered": 13, "failed": 0, "unconfirmed": 1, "cost_eur": 1.008, "cost_mad": 11.088 }
  ],
  "error_diagnostics": [],
  "waba_breakdown": []
}
DocsAPI Keys/api-keys
GEThttps://api.envoisms.ma/v1/api-keys
Bearer

List API keys

Returns the account’s API keys, active and revoked, never with their full value.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: api_keys.
  • permissions and ip_whitelist are JSON strings (ip_whitelist is null when unrestricted).
GEThttps://api.envoisms.ma/v1/api-keys
curl -X GET "https://api.envoisms.ma/v1/api-keys" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "data": [
    {
      "id": "key_e84c2a6b8d0f1e3c5a7b9d2f",
      "name": "Serveur de production",
      "key_prefix": "smr_a8f7d6c5",
      "ip_whitelist": "[\"196.200.1.4\"]",
      "rate_limit": 180,
      "permissions": "[\"send\",\"verify\",\"status\",\"balance\"]",
      "active": 1,
      "last_used_at": "2026-10-22 09:14:03",
      "created_at": "2026-10-01 08:00:00",
      "sandbox": false
    }
  ]
}
DocsAPI Keys/api-keys
POSThttps://api.envoisms.ma/v1/api-keys
Bearer

Create an API key

Creates a live (smr_…) or sandbox (env_test_…) key. Returns 201; the full value (api_key) is shown once.

Request body (JSON)
FieldTypeRequirementDescription
namestringOptionalKey label (default: "API key").
permissionsstring[]OptionalAmong send, verify, status, balance, contacts, campaigns, webhooks, billing, analytics, api_keys, optouts, sender_id. Default: send, verify, status, balance. Unknown values are ignored.
ip_whiteliststring[]OptionalThe only IP addresses allowed to use the key.
rate_limitintegerOptionalRequests per minute, 10 to 2000. Default by plan: Starter 60, Business 180, Pro 500, Enterprise 1,200.
sandboxbooleanOptionaltrue creates a test key: requests are validated and recorded, nothing is sent, nothing is billed. A key’s mode is permanent.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: api_keys.
  • The number of active live keys depends on the plan, and sandbox keys have their own cap; beyond it, 403 LIMIT_EXCEEDED. A sandbox key adds "note" to the response.
POSThttps://api.envoisms.ma/v1/api-keys
curl -X POST "https://api.envoisms.ma/v1/api-keys" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "OTP backend",
    "permissions": [
      "verify",
      "status"
    ],
    "sandbox": true
  }'
{
  "id": "key_e84c2a6b8d0f1e3c5a7b9d2f",
  "name": "Serveur de production",
  "key_prefix": "smr_a8f7d6c5",
  "api_key": "smr_a8f7d6c5…",
  "permissions": ["send", "verify", "status", "balance"],
  "rate_limit": 180,
  "sandbox": false,
  "warning": "The full API key is shown once. Store it securely."
}
DocsAPI Keys/api-keys/:id
PATCHhttps://api.envoisms.ma/v1/api-keys/:id
Bearer

Update an API key

Updates a key’s name, permissions, allowed IPs, rate limit or active state.

Request body (JSON)
FieldTypeRequirementDescription
name | permissions | ip_whitelistmixedOptionalSame rules as at creation.
rate_limitintegerOptionalRequests per minute, 10 to 1000.
activebooleanOptionalfalse suspends the key.
Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: api_keys.
  • Sandbox or live mode cannot change: 400 SANDBOX_IMMUTABLE. Create a new key.
PATCHhttps://api.envoisms.ma/v1/api-keys/:id
curl -X PATCH "https://api.envoisms.ma/v1/api-keys/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "OTP backend (rotated)"
  }'
{
  "updated": true,
  "id": "key_e84c2a6b8d0f1e3c5a7b9d2f"
}
DocsAPI Keys/api-keys/:id
DELETEhttps://api.envoisms.ma/v1/api-keys/:id
Bearer

Revoke an API key

Permanently revokes a key: it no longer authenticates any request.

Headers
Authorization: Bearer smr_…
Content-Type: application/json
Accept: application/json
Good to know
  • Required API key permission: api_keys.
DELETEhttps://api.envoisms.ma/v1/api-keys/:id
curl -X DELETE "https://api.envoisms.ma/v1/api-keys/{id}" \
  -H "Authorization: Bearer $ENVOISMS_API_KEY"
{
  "revoked": true,
  "id": "key_e84c2a6b8d0f1e3c5a7b9d2f"
}