Python

Send SMS from Python with the REST API and requests.

Environment variables

Create a key from the dashboard with the sms:send scope, and have an SMS quota and an approved sender. Keep the key in COUSSEMA_API_KEY, never in shared code, a URL or logs.

Also set COUSSEMA_SMS_TO, COUSSEMA_SMS_MESSAGE, COUSSEMA_SENDER_NAME, COUSSEMA_CLIENT_REFERENCE and COUSSEMA_IDEMPOTENCY_KEY with the data of the send you want. Python does not load a .env file automatically.

Create one unique idempotency key (for example a UUID) per operation and store it with the content before the call. A retry must find these same values, even after a restart. Running the script calls the production API and uses quota if the send is accepted.

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

Send an SMS

Save this example as send_sms.py, then run python send_sms.py. The script makes a single call with a timeout and no automatic retry.

FieldDescription
toRecipient number in international format, for example 243…; one recipient per request.
messageText of 1 to 1,530 characters. The quota used depends on the SMS segments and the encoding.
senderNameSender name approved for your organization. You can use senderId (UUID) instead.
clientReferenceOptional business reference, 1 to 128 characters. It does not replace the 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'],
    })

202 response

202 Accepted confirms queueing; it does not confirm delivery. Illustrative example:

Store message.id and your clientReference. The provider.messageId field stays null. Follow final delivery through the webhooks or the DLR reports in the 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 }
}

Errors and retries

The idempotency window is 24 hours. After it, do not replay an uncertain result without checking the existing messages or asking support. A business reference alone does not deduplicate sends.

  • 400 / 422: check the content and the sender before a new, corrected send.
  • 401 / 403: check the key, its permissions and the organization’s access. Do not loop on these errors.
  • 409: the same key is already being processed or was used with other content. Check the existing operation; do not work around the conflict with a new key.
  • 429: respect Retry-After when present. Keep the same key and the same content when retrying this operation.
  • 5xx, timeout or unreadable response: the outcome may be uncertain. Keep the data and the x-request-id to check the result; do not create a second send automatically.

Verify webhooks

Create your HTTPS endpoint through POST /v1/webhooks with the webhooks:write scope, a url and subscribed_events: ["message.delivered", "message.failed"]. Keep the returned signingSecret in COUSSEMA_WEBHOOK_SECRET, separately from the API key.

In your Django, Flask or other framework route, call verify_webhook(raw_body, headers, os.environ['COUSSEMA_WEBHOOK_SECRET']) before processing the JSON. Pass the original body bytes and reject an invalid signature.

Match message.id and message.client_reference with your operation. Store the callback durably, deduplicate x-coussema-delivery-id, then quickly return a 2xx. A new delivery of the same callback must not trigger your business action twice.

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)