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é.

bash
python3 -m venv .venv
# macOS / Linux
source .venv/bin/activate
# Windows PowerShell : .venv\Scripts\Activate.ps1
python -m pip install requests

Envoyer 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.

ChampDescription
toNuméro du destinataire au format international, par exemple 243… ; un destinataire par requête.
messageTexte de 1 à 1 530 caractères. Le quota consommé dépend des segments SMS et de l’encodage.
senderNameNom d’expéditeur approuvé pour votre organisation. Vous pouvez utiliser senderId (UUID) à sa place.
clientReferenceRéférence métier facultative, de 1 à 128 caractères. Elle ne remplace pas Idempotency-Key.
python
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.

json
{
  "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-After lorsqu’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-id pour 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.

python
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)