sage-messaging (Python)
SDK officiel de l'API Sage Messaging : envoi et lecture de messages WhatsApp et SMS, vérification des webhooks.
pip install sage-messaging
Python 3.9+. Client synchrone et asynchrone.
Démarrage
from sage_messaging import SageMessaging
client = SageMessaging(api_key="sk_...") # ou variable SAGE_MESSAGING_API_KEY
# WhatsApp (canal par défaut)
client.messages.send(phone="+2250787572030", message="Bonjour !")
# SMS
client.sms.send(phone="+2250787572030", message="Votre code : 4821")
Asynchrone
from sage_messaging import AsyncSageMessaging
async with AsyncSageMessaging() as client:
await client.messages.send(phone="+2250787572030", message="Bonjour !")
Les méthodes sont identiques ; il suffit de les await.
Configuration
| Paramètre | Variable d'environnement | Défaut |
|---|---|---|
api_key |
SAGE_MESSAGING_API_KEY |
— |
base_url |
SAGE_MESSAGING_BASE_URL |
https://messaging.sagecoders.com |
webhook_secret |
SAGE_MESSAGING_WEBHOOK_SECRET |
— |
timeout |
30 secondes |
|
max_retries |
2 (lectures uniquement) |
Les lectures (GET) sont rejouées en cas d'erreur réseau, de 429 ou de 5xx. Les envois ne sont jamais rejoués : un envoi dont la réponse s'est perdue a pu partir, et le rejouer enverrait le message en double.
Envoi
# Message texte, WhatsApp ou SMS
client.messages.send(phone="+2250787572030", message="Salut", channel="sms")
# SMS différé, depuis un téléphone précis
from datetime import datetime, timedelta, timezone
client.sms.send(
phone="+2250787572030",
message="Rappel : RDV demain",
from_="+2250555884378", # voir client.sms.phones()
send_at=datetime.now(timezone.utc) + timedelta(hours=2), # 20 jours max
)
# Campagne SMS (1 à 100 destinataires)
client.sms.bulk_send(phones=["+2250787572030", "+2250555884378"], message="Promo -20 %")
# Médias WhatsApp (1 à 10, 16 Mo max chacun), envoyés dans l'ordre
res = client.messages.send_media(
phone="+2250787572030",
caption="Votre facture",
media=[
{"path": "facture.pdf"}, # fichier local
{"url": "https://exemple.com/photo.jpg"}, # URL publique
{"data": image_bytes, "filename": "capture.png"}, # octets
],
)
if res["failed"]: # succès partiel (HTTP 207)
print("Échecs :", res["failed"])
Utilisez des datetime avec fuseau horaire pour send_at et since.
Lecture
client.conversations.list(unread_only=True, limit=20)
client.conversations.messages(42, direction="received")
client.conversations.mark_read(42)
client.messages.list(after_id=1000) # flux global, id croissant
Les réponses paginées ont la forme {"data": [...], "meta": {"current_page", "per_page", "total", "last_page"}}.
Réponses citées
Quand un contact répond sur WhatsApp à un message précis, quoted_message_id donne l'id de ce message — le même que le message_id renvoyé par messages.send, ou sent[0]["message_id"] (la pièce qui porte la légende) pour messages.send_media. Il vaut None si ce n'est pas une citation ou si le message cité n'a pas été envoyé via SageMessaging.
Fichiers reçus (WhatsApp)
Un message reçu avec un fichier porte media: {"mimetype", "filename", "size"}. Le fichier n'est pas stocké : il est récupéré chez WhatsApp à la demande, et n'est disponible que tant que WhatsApp le conserve — téléchargez-le dès réception.
if msg.get("media"):
file = client.messages.download_media(msg["id"])
Path(file["filename"] or f"media-{msg['id']}").write_bytes(file["data"])
Erreurs : 404 sans fichier, 410 expiré chez WhatsApp, 413 au-delà de la limite (16 Mo, 100 Mo pour un document), 503 WhatsApp déconnecté.
Suivre les nouveaux messages sans webhook
for msg in client.messages.poll(direction="received", interval=5):
print(msg["contact_phone"], msg["body"])
Sans after_id, seuls les messages arrivés après l'appel sont renvoyés. Pour reprendre après un redémarrage, conservez le dernier msg["id"] et passez-le en after_id.
Organisations clientes (multi-clients)
Chaque entreprise cliente devient une organisation de votre compte, avec son propre WhatsApp et son propre téléphone SMS. La clé API désigne l'organisation : les envois partent de ses canaux ou, tant qu'elle n'en a pas, de ceux de votre compte. Vos clients n'ont pas besoin de compte SageMessaging : ils scannent les QR codes depuis votre application.
# Clé du compte avec la permission organizations:manage
admin = SageMessaging()
# 1. Créer l'organisation : sa clé smsk-… n'est renvoyée qu'ici, conservez-la
created = admin.organizations.create(
name="Hôtel Paradisias",
abilities=["messages:send", "sms:send", "channels:manage"],
)
# 2. Avec la clé de l'organisation : QR codes à afficher à votre client
client = SageMessaging(api_key=created["api_key"])
wa = client.whatsapp.connect()
if wa["state"] == "connecting":
show_qr(wa["qrcode"]) # data-URL PNG, expire en ~20 s : redemandez-le
# … jusqu'à ce que client.whatsapp.status()["connected"] soit vrai
sms = client.sms.provision() # {"qrcode", "server_url"} pour l'application Android
# Plus tard : client.sms.qrcode() le réaffiche, client.sms.status() confirme le téléphone
# 3. Envoyer depuis le téléphone de l'organisation
client.messages.send(phone="+2250787572030", message="Bonjour !")
Gérer les organisations et leurs clés :
orgs = admin.organizations.list() # la vôtre (is_default) en premier
org_id = created["organization"]["id"]
key = admin.organizations.keys.create(org_id, name="Back-office", abilities=["messages:send"])
# key["api_key"] : affichée une seule fois
admin.organizations.keys.list(org_id) # jamais la clé elle-même
admin.organizations.keys.revoke(org_id, key["id"])
client.whatsapp.logout() # déconnecte le téléphone de l'organisation
Une clé créée par API n'a jamais plus de permissions que la clé qui la crée, et ne reçoit jamais organizations:manage. Le client asynchrone expose les mêmes méthodes (await client.organizations.create(...)).
Webhooks
Chaque webhook est signé en HMAC-SHA256 (X-Signature, X-Timestamp). Passez le corps brut de la requête, pas un JSON re-sérialisé.
from sage_messaging import verify_webhook, WebhookVerificationError
# FastAPI
@app.post("/webhooks/sage")
async def sage_webhook(request: Request):
try:
event = verify_webhook(await request.body(), request.headers, SECRET)
except WebhookVerificationError:
raise HTTPException(400)
if event["event"] == "message.received":
msg = event["data"]["message"]
contact = event["data"]["conversation"]
...
return {"ok": True}
Django : verify_webhook(request.body, request.headers, SECRET) ; Flask : verify_webhook(request.get_data(), request.headers, SECRET).
Les requêtes de plus de 5 minutes sont refusées (tolerance=300) pour empêcher leur rejeu.
Erreurs
from sage_messaging import ValidationError, GatewayError, SageMessagingError
try:
client.sms.send(phone="+2250787572030", message="...")
except ValidationError as e: # 422 : champ invalide, canal non provisionné…
print(e.errors, e.channel)
except GatewayError as e: # 502 : la passerelle a refusé l'envoi
print(e.detail)
except SageMessagingError as e: # toutes les autres erreurs du SDK
print(e)
| Classe | Cas |
|---|---|
AuthenticationError |
401 : clé absente ou invalide |
PermissionDeniedError |
403 : la clé n'a pas la permission requise |
NotFoundError |
404 |
ValidationError |
422 |
RateLimitError |
429 |
GatewayError |
502 : refus de WhatsApp ou de la passerelle SMS |
APIError |
classe mère des erreurs HTTP (.status, .body) |
APIConnectionError |
réseau, DNS, délai dépassé |
WebhookVerificationError |
signature absente, invalide ou expirée |
Permissions des clés API
| Permission | Méthodes |
|---|---|
messages:send |
messages.send, messages.send_media (WhatsApp) |
sms:send |
sms.* et messages.send(channel="sms") |
messages:read |
messages.list, messages.poll, conversations.*, sms.phones |
channels:manage |
whatsapp.*, sms.provision, sms.qrcode, sms.status |
organizations:manage |
organizations.*, organizations.keys.* |
Metadata
Release files for sage-messaging 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| sage_messaging-0.3.0.tar.gz | 17.8 kB | Without provenance | Details for sage_messaging-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sage_messaging-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for sage_messaging-0.3.0-py3-none-any.whl |
Total release size: 38.7 kB
Release files / sage_messaging-0.3.0.tar.gz
| Download URL | sage_messaging-0.3.0.tar.gz |
|---|---|
| Size | 17.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
32de6c1ef11189b30fbeb290456f7eb1d0662e993bb5777084cf9254734f80e5
|
|
BLAKE2b-256 checksum How to use checksums |
2a809f0331097b70c4bbd97dc8bc6106c1fce59ff16f06447316b17dfa8aaf26
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / sage_messaging-0.3.0-py3-none-any.whl
| Download URL | sage_messaging-0.3.0-py3-none-any.whl |
|---|---|
| Size | 20.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
973d7cc25b65386cd33747bf70aba1d1e1991dc0d3a3c9444ef7320344f2bd80
|
|
BLAKE2b-256 checksum How to use checksums |
a55c073fe8122dca6a6092764e2752ca3b69f48d6083070fa73863a7c02ceb8e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Security for sage-messaging 0.3.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.