Python
Envoyez des SMS depuis Python avec l’API REST et requests.
Variables d’environnement
Créez une clé depuis le dashboard avec le droit sms:send, disposez d’un quota SMS et d’un expéditeur approuvé. Conservez la clé dans COUSSEMA_API_KEY, jamais dans le code partagé, une URL ou les journaux.
Configurez également COUSSEMA_SMS_TO, COUSSEMA_SMS_MESSAGE, COUSSEMA_SENDER_NAME, COUSSEMA_CLIENT_REFERENCE et COUSSEMA_IDEMPOTENCY_KEY avec les données de l’envoi voulu. Python ne charge pas automatiquement un fichier .env.
Créez une clé d’idempotence unique (par exemple un UUID) par opération et enregistrez-la avec le contenu avant l’appel. Une reprise doit retrouver ces mêmes valeurs, même après un redémarrage. L’exécution du script appelle l’API de production et consomme du quota si l’envoi est accepté.
python3 -m venv .venv
# macOS / Linux
source .venv/bin/activate
# Windows PowerShell : .venv\Scripts\Activate.ps1
python -m pip install requestsEnvoyer un SMS
Enregistrez cet exemple dans send_sms.py, puis lancez python send_sms.py. Le script effectue un seul appel avec un délai d’attente et sans relance automatique.
| Champ | Description |
|---|---|
| to | Numéro du destinataire au format international, par exemple 243… ; un destinataire par requête. |
| message | Texte de 1 à 1 530 caractères. Le quota consommé dépend des segments SMS et de l’encodage. |
| senderName | Nom d’expéditeur approuvé pour votre organisation. Vous pouvez utiliser senderId (UUID) à sa place. |
| clientReference | Référence métier facultative, de 1 à 128 caractères. Elle ne remplace pas Idempotency-Key. |
import os
import uuid
import requests
def send_sms(to, message, sender_name, idempotency_key, client_reference):
api_key = os.environ['COUSSEMA_API_KEY'].strip()
if not api_key or not idempotency_key.strip():
raise ValueError('Clé API et Idempotency-Key requises.')
# Ces valeurs doivent rester identiques pour reprendre le même envoi.
payload = {
'to': to,
'message': message,
'senderName': sender_name,
'clientReference': client_reference,
}
try:
response = requests.post(
'https://api.coussema.com/v1/sms/send',
headers={
'Authorization': f'Bearer {api_key}',
'Idempotency-Key': idempotency_key,
'X-Request-Id': str(uuid.uuid4()),
},
json=payload,
timeout=(5, 30),
allow_redirects=False,
)
except requests.exceptions.RequestException as exc:
raise RuntimeError(
'Résultat réseau incertain. Conserver la même clé et le même '
'contenu pour une reprise contrôlée, sans nouvel envoi automatique.'
) from exc
request_id = response.headers.get('x-request-id', '')
if not 200 <= response.status_code < 300:
# Ne pas journaliser la clé API, les headers ou le contenu du SMS.
raise RuntimeError(
f'HTTP {response.status_code}; request_id={request_id}; '
f'Retry-After={response.headers.get("Retry-After", "")}. '
'Consulter les règles de reprise avant de retenter.'
)
try:
result = response.json()
except ValueError as exc:
raise RuntimeError(
f'Réponse illisible; request_id={request_id}. '
'Ne pas créer une nouvelle Idempotency-Key.'
) from exc
if (
not isinstance(result, dict)
or result.get('success') is not True
or result.get('queued') is not True
or result.get('status') != 'accepted'
or not isinstance(result.get('message'), dict)
or not result['message'].get('id')
):
raise RuntimeError(
f'Réponse inattendue; request_id={request_id}. '
'Vérifier le résultat avant toute reprise.'
)
return result
if __name__ == '__main__':
# Configurer ces variables sur le serveur avant de lancer le script.
# La clé d'idempotence est créée ET enregistrée par votre application
# avant le premier appel, puis réutilisée pour cette même opération.
result = send_sms(
to=os.environ['COUSSEMA_SMS_TO'],
message=os.environ['COUSSEMA_SMS_MESSAGE'],
sender_name=os.environ['COUSSEMA_SENDER_NAME'],
idempotency_key=os.environ['COUSSEMA_IDEMPOTENCY_KEY'],
client_reference=os.environ['COUSSEMA_CLIENT_REFERENCE'],
)
print({
'message_id': result['message']['id'],
'status': result['status'],
'remaining_quota': result['remainingQuota'],
})Réponse 202
202 Accepted confirme la mise en file ; il ne confirme pas la livraison. Exemple illustratif :
Enregistrez message.id et votre clientReference. Le champ provider.messageId reste null. La livraison finale se suit via les webhooks ou les rapports DLR du dashboard.
{
"success": true,
"count": 1,
"queued": true,
"status": "accepted",
"remainingQuota": 999,
"message": {
"id": "a2f7e5cb-bbe9-4d7d-bd17-80d1d7a5e5af",
"clientReference": "commande-2026-0001"
},
"provider": { "code": "coussema", "messageId": null, "httpStatus": 202 }
}Erreurs et reprises
La fenêtre d’idempotence est de 24 heures. Après cette fenêtre, ne rejouez pas un résultat incertain sans rapprochement avec les messages existants ou le support. Une référence métier seule ne déduplique pas les envois.
- 400 / 422 : vérifier le contenu et l’expéditeur avant un nouvel envoi corrigé.
- 401 / 403 : vérifier la clé, ses permissions et l’accès de l’organisation. Ne pas boucler sur ces erreurs.
- 409 : la même clé est déjà en traitement ou a été utilisée avec un autre contenu. Vérifier l’opération existante ; ne pas contourner le conflit avec une nouvelle clé.
- 429 : respecter
Retry-Afterlorsqu’il est présent. Conserver la même clé et le même contenu lors de la reprise de cette opération. - 5xx, timeout ou réponse illisible : l’issue peut être incertaine. Conserver les données et
x-request-idpour vérifier le résultat ; ne pas créer un second envoi automatiquement.
Vérifier les webhooks
Créez votre endpoint HTTPS via POST /v1/webhooks avec le droit webhooks:write, une url et subscribed_events: ["message.delivered", "message.failed"]. Conservez le signingSecret retourné dans COUSSEMA_WEBHOOK_SECRET, séparément de la clé API.
Dans votre route Django, Flask ou autre framework, appelez verify_webhook(raw_body, headers, os.environ['COUSSEMA_WEBHOOK_SECRET']) avant de traiter le JSON. Transmettez les octets originaux du corps et rejetez une signature invalide.
Rapprochez message.id et message.client_reference avec votre opération. Enregistrez durablement le callback, dédupliquez x-coussema-delivery-id, puis retournez rapidement un 2xx. Une nouvelle livraison du même callback ne doit pas déclencher deux fois votre action métier.
import hashlib
import hmac
import json
from datetime import datetime, timezone
def verify_webhook(raw_body, headers, signing_secret):
# raw_body : bytes reçus AVANT tout décodage JSON.
headers = {key.lower(): value for key, value in headers.items()}
timestamp = headers.get('x-coussema-timestamp', '')
signature = headers.get('x-coussema-signature', '')
if not signing_secret or not timestamp or not signature:
raise ValueError('Signature ou secret manquant.')
try:
sent_at = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
if sent_at.tzinfo is None:
raise ValueError('Fuseau horaire manquant.')
age = abs((datetime.now(timezone.utc) - sent_at).total_seconds())
except (ValueError, OverflowError) as exc:
raise ValueError('Horodatage invalide.') from exc
# Tolérance choisie pour cet exemple : cinq minutes.
if age > 300:
raise ValueError('Signature expirée.')
digest = hmac.new(
signing_secret.encode('utf-8'),
timestamp.encode('utf-8') + b'.' + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(
('v1=' + digest).encode('ascii'), signature.encode('utf-8')
):
raise ValueError('Signature invalide.')
return json.loads(raw_body)