Skip to main content

EnvoiSMS Python SDK

PyPI version Python versions License: MIT

Official Python SDK for EnvoiSMS.ma — the direct-operator SMS, WhatsApp Business (WABA) and OTP verification API platform for Morocco (Maroc). One client, one API key, four channels.

pip install envoisms

What is EnvoiSMS.ma?

EnvoiSMS.ma routes transactional and marketing messages through direct connections to Morocco's three mobile operators, plus the official WhatsApp Cloud API — no aggregator, no gray SIM routes.

Channel What it's for Covered by this SDK
SMS Direct Opérateurs — IAM, Inwi, Orange OTP codes, delivery alerts, marketing SMS, from 0.48 MAD/SMS ✅ send(), send_bulk()
WhatsApp Business API (Meta WABA) Approved templates, interactive buttons, catalog, multi-agent inbox, from 0.65 MAD/message ✅ send(channel="whatsapp")
OTP / 2FA Verification Send + check one-time codes over SMS or WhatsApp ✅ send_otp(), check_otp()
Numéro Virtuel (+212) Cloud Moroccan business line, no physical SIM, shared team inbox Manage from the dashboard
Assistant IA Conversationnel Darija/French AI agent for COD order confirmation & support handoff Manage from the dashboard

Numéros Virtuels and the AI assistant are configured from your EnvoiSMS.ma dashboard today; dedicated SDK endpoints for them are on the roadmap. Everything below (send, OTP, billing, webhooks) works with the SDK right now.

Quick Start — Send an SMS

import os
from envoisms import EnvoiSMSClient

client = EnvoiSMSClient(api_key=os.getenv("ENVOISMS_API_KEY"))

response = client.send(
    to="+212600000000",
    message="Votre code de vérification est 492018",
    from_sender="MonBusiness",  # validated Sender ID, or omit to use your default
)

print(f"Message ID: {response['id']}")  # poll it with get_message(), match it in webhooks

Every send() / send_bulk() carries an Idempotency-Key (generated, or pass idempotency_key=), so a retry after a timeout can never bill the same message twice.

Choosing a channel: SMS or WhatsApp?

Default to SMS. It reaches every Moroccan mobile (IAM, Inwi, Orange) with no setup beyond your API key, and it is what an "ordinary text to a customer" needs — even when that customer uses WhatsApp.

channel="whatsapp" is different in kind, not just in name. It sends from your own WhatsApp Business number, which means:

  • the number must be connected in your dashboard (WhatsApp tab) — otherwise the API answers 403 WHATSAPP_NOT_CONNECTED and nothing is charged;
  • a free-form text is only accepted while the recipient has written to that number in the last 24 hours (400 OUT_OF_24H_WINDOW otherwise, nothing charged);
  • outside that window, you send an approved template (template field), not free text.
You want to… Use
Send a text to a customer (order status, reminder, alert) channel="sms" (the default — just omit it)
Send a one-time code send_otp() — pass channel="whatsapp" for a WhatsApp code through our shared sender, no connection needed
Reply on WhatsApp to a customer who wrote to your number in the last 24 h channel="whatsapp" with message
Start a WhatsApp conversation (marketing, utility) channel="whatsapp" with an approved template

Common mistake: sending an SMS-style text with channel="whatsapp" "because the customer is on WhatsApp". Both refusals above name the fix — send it as SMS.

Send a WhatsApp Business Message

Only from a WhatsApp Business number you connected in your dashboard — see the table above. Free text works inside the 24-hour customer window; otherwise send an approved template.

# Reply to a customer who wrote to your number in the last 24 h
response = client.send(
    to="+212600000000",
    message="Bonjour ! Votre commande #89240 a été expédiée.",
    channel="whatsapp",
)

# Start the conversation yourself: approved template, any time
response = client.send(
    to="+212600000000",
    message="Votre commande #89240 a été expédiée.",  # shown in your history; the template body is what goes out
    channel="whatsapp",
    template={"name": "order_shipped", "language": "fr", "variables": ["89240"]},
)

Automatic channel fallback (cascade)

Send over WhatsApp and drop back to SMS automatically when a number is unreachable or has no WhatsApp — the same fallback used for VTC riders on flaky mobile data.

response = client.send(
    to="+212600000000",
    message="Votre chauffeur arrive dans 2 minutes.",
    channel="whatsapp",
    cascade=True,
)

OTP / 2FA Verification

# 1. Send OTP (channel="sms" by default; pass channel="whatsapp" to send over WhatsApp instead)
otp_res = client.send_otp(
    to="+212600000000",
    brand="MonBusiness",
    code_length=6,
    expiry=600,  # seconds
)

session_id = otp_res["session_id"]

# 2. Verify the code the user typed in
check_res = client.check_otp(session_id=session_id, code="492018")

if check_res.get("verified"):
    print("OTP code is valid!")

# Optional: inspect a session's status without consuming an attempt
session = client.get_otp_session(session_id)

Bulk Sending

client.send_bulk(
    messages=[
        {"to": "+212600000001", "message": "Promo -20% ce week-end"},
        {"to": "+212600000002", "message": "Promo -20% ce week-end"},
    ],
    from_sender="MonBusiness",
)

Message Status & Delivery

status = client.get_message(message_id="msg_123")
recent = client.list_messages(limit=50, offset=0)

Verifying Delivery Webhooks (DLR)

If you configure a delivery-status webhook, verify its X-EnvoiSMS-Signature header before trusting the payload:

from envoisms import EnvoiSMSClient

is_valid = EnvoiSMSClient.verify_webhook_signature(
    raw_body=request.body,             # raw request bytes/string, not parsed JSON
    signature_header=request.headers["X-EnvoiSMS-Signature"],
    secret=os.getenv("ENVOISMS_WEBHOOK_SECRET"),
)

Account & Billing

balance = client.get_balance()
packs = client.list_packs()
payment_methods = client.list_payment_methods()

client.create_topup(amount_mad=200, payment_method="stripe")

Analytics & API Keys

stats = client.analytics(days=30)
new_key = client.create_api_key(name="Server key", scope="live")

Compliance: Opt-outs (STOP)

client.create_optout(phone="+212600000000")

Error Handling & Retries

The client retries 5xx responses and network errors up to max_retries times (default 2) with exponential backoff, and raises EnvoiSMSError — with status_code and code attributes — on any failure:

from envoisms import EnvoiSMSClient, EnvoiSMSError

client = EnvoiSMSClient(api_key=os.getenv("ENVOISMS_API_KEY"), max_retries=3, timeout=20)

try:
    client.send(to="+212600000000", message="Test")
except EnvoiSMSError as e:
    print(f"Send failed ({e.status_code} {e.code}): {e}")

Why teams pick EnvoiSMS.ma over an aggregator

  • Direct routes to IAM, Inwi and Orange — no international transit hop, no gray-route ban risk.
  • 2.4–2.8 second OTP latency, measured across IAM, Inwi and Orange — aggregators routing through Europe typically land in the 10s+ range.
  • Billing in MAD, no EUR/USD conversion surprises.
  • Local support based in Casablanca, not an offshore ticket queue.

See the full breakdown on envoisms.ma.

Documentation & Pricing

Other official SDKs

Support

License

MIT

Release files for envoisms 1.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for envoisms 1.2.0
File Size Uploaded
envoisms-1.2.0.tar.gz 9.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for envoisms 1.2.0
File Interpreter ABI Platform
envoisms-1.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 18.1 kB

Release files / envoisms-1.2.0.tar.gz

Download URL envoisms-1.2.0.tar.gz
Size 9.1 kB
Tags Source
SHA-256 checksum
How to use checksums
1d7cb4ce62c043a7797366b6496ef37df8af11735d445f8d03a4f8f03fca3ee9
BLAKE2b-256 checksum
How to use checksums
dc131ac2c032130fdad9d4c990fd9721395f43348d22773a4fca79d61ea813d2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release files / envoisms-1.2.0-py3-none-any.whl

Download URL envoisms-1.2.0-py3-none-any.whl
Size 8.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e60a7f01d753a01bac9090c10fb52767de2eb7692ca8855019040eaec8eee3a0
BLAKE2b-256 checksum
How to use checksums
632e56894c72fb653f8dac83d871a83161d868a6ef71796776e5ec7105eac6f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.2.0 This release

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page