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.
python3 -m venv .venv
# macOS / Linux
source .venv/bin/activate
# Windows PowerShell : .venv\Scripts\Activate.ps1
python -m pip install requestsSend 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.
| Field | Description |
|---|---|
| to | Recipient number in international format, for example 243…; one recipient per request. |
| message | Text of 1 to 1,530 characters. The quota used depends on the SMS segments and the encoding. |
| senderName | Sender name approved for your organization. You can use senderId (UUID) instead. |
| clientReference | Optional business reference, 1 to 128 characters. It does not replace the 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'],
})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.
{
"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-Afterwhen 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-idto 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.
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)