This tutorial sends an SMS to a Tunisian number from a Python script, using the requests library and the Sendimo REST API. It works the same way in a Django, Flask or FastAPI app: put the call in your view or background task.
What you need
- Python 3.9 or later;
- the
requestslibrary:pip install requests; - a Sendimo API key. Start with an
sk_test_test key: responses are realistic, but nothing is sent or charged.
Keep the key out of your code, in an environment variable:
export SENDIMO_API_KEY="sk_test_..."
1. Send the SMS
A send is one POST /v1/messages request with the number in international format, the sender name and the text.
import os
import requests
response = requests.post(
"https://api.sendimo.tn/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['SENDIMO_API_KEY']}",
"Idempotency-Key": "order-4821-otp",
},
json={
"to": "+21620123456",
"from": "SENDIMO",
"text": "Your code is 482913. It expires in 5 minutes.",
"reference": "order-4821",
},
timeout=10,
)
response.raise_for_status() # 4xx/5xx -> exception
message = response.json()
print(message["id"], message["status"]) # msg_01HF8Z3K9QX2 queued
The API answers 202 Accepted: the message is accepted and queued for sending.
{
"id": "msg_01HF8Z3K9QX2",
"status": "queued",
"to": "+21620123456",
"from": "SENDIMO",
"encoding": "GSM-7",
"segments": 1,
"network": "ooredoo",
"cost": { "amount": "0.140", "currency": "TND" },
"reference": "order-4821"
}
Three fields deserve your attention:
to: always in E.164 format, with+216;from: a sender name approved on your account;Idempotency-Key: if your script retries after a network failure, the same key guarantees the SMS is sent only once.
2. Test without sending anything
With an sk_test_ key, two numbers simulate the two possible outcomes:
+21600000001always endsdelivered;+21600000002always endsfailed.
You can test your whole flow, errors included, before switching to an sk_live_ key.
3. Handle errors
Every error has the same shape, with a stable code:
try:
response.raise_for_status()
except requests.HTTPError:
error = response.json()["error"]
if error["code"] == "insufficient_balance":
print("Not enough balance: top up your account.")
elif error["code"] == "invalid_phone_number":
print("Invalid number:", error["message"])
else:
print(error["code"], error["request_id"])
The most common codes:
| HTTP | Code | Cause |
|---|---|---|
| 400 | invalid_phone_number |
to is not a valid number |
| 401 | unauthorized |
Bad, missing or revoked API key |
| 402 | insufficient_balance |
Not enough balance for the send |
| 403 | sender_not_approved |
from is not an approved sender name |
| 429 | rate_limited |
Too many requests; wait for Retry-After |
On a 500 or 503 error, retry with the same Idempotency-Key.
4. Track delivery
The status moves from queued to sent, then to delivered or failed. Two ways to follow it:
- poll
GET /v1/messages/{id}; - better: receive the
message.deliveredormessage.failedevent on your webhook, without polling.
How much does it cost?
This SMS is 45 characters in GSM-7: one segment, billed at your plan's rate, from 0.100 DT to 0.150 DT. The details are in our article on what an SMS costs in Tunisia.
Read next
Subscribe to get daily insights and company news straight to your inbox.