Skip to content

Python

Python 3.9+. Either requests (the go-to for sync code) or httpx (sync

  • async, same surface). Examples in both.
Terminal window
pip install requests
# or
pip install httpx
import os
API_KEY = os.environ["LISOLOO_API_KEY"]
API_URL = "$BASE_URL/api/v1/lisoloo/sms-api"
HEADERS = {"app-key": API_KEY, "Content-Type": "application/json"}
import requests
r = requests.post(
f"{API_URL}/send",
headers=HEADERS,
json={
"to": ["+243998857000"],
"message": "Hello from Lisoloo!",
"sender_id": "MYAPP",
},
timeout=10,
)
r.raise_for_status()
data = r.json()["data"]
print(data["message_id"])
from dataclasses import dataclass
from typing import Iterable, Optional
import requests
@dataclass
class LisolooClient:
api_key: str
base_url: str = "$BASE_URL/api/v1/lisoloo/sms-api"
timeout: float = 10.0
def _headers(self) -> dict:
return {"app-key": self.api_key, "Content-Type": "application/json"}
def send(
self,
to: Iterable[str],
message: str,
sender_id: Optional[str] = None,
sending_type: str = "immediate",
scheduled_dates: Optional[list] = None,
recurring_schedule: Optional[dict] = None,
callback_url: Optional[str] = None,
) -> dict:
body = {"to": list(to), "message": message, "sending_type": sending_type}
if sender_id: body["sender_id"] = sender_id
if callback_url: body["callback_url"] = callback_url
if scheduled_dates: body["scheduled_dates"] = scheduled_dates
if recurring_schedule: body["recurring_schedule"] = recurring_schedule
r = requests.post(f"{self.base_url}/send", json=body,
headers=self._headers(), timeout=self.timeout)
r.raise_for_status()
return r.json()["data"]
def get_status(self, message_id: str) -> dict:
r = requests.get(f"{self.base_url}/status/{message_id}",
headers=self._headers(), timeout=self.timeout)
r.raise_for_status()
return r.json()["data"]
def get_balance(self) -> dict:
r = requests.get(f"{self.base_url}/balance",
headers=self._headers(), timeout=self.timeout)
r.raise_for_status()
return r.json()["data"]

Usage:

client = LisolooClient(api_key=os.environ["LISOLOO_API_KEY"])
# Instant send
result = client.send(["+243998857000"], "Hello!", sender_id="MYAPP")
print(result["message_id"])
# Scheduled
client.send(
["+243998857000"], "Reminder",
sending_type="scheduled",
scheduled_dates=[{"date": "2026-06-01", "time": "08:00"}],
)
# Recurring
client.send(
["+243998857000"], "Weekly check-in",
sending_type="recurring",
recurring_schedule={
"start_date": "2026-06-01", "end_date": "2026-12-31",
"frequency": "weekly", "interval": 1, "times": ["09:00"],
},
)
import requests
try:
result = client.send(["+243998857000"], "Hi")
except requests.HTTPError as e:
body = e.response.json()
code = body.get("error_code")
if code == "1301": # rate-limited
retry_after = int(e.response.headers.get("Retry-After", 60))
time.sleep(retry_after)
result = client.send(["+243998857000"], "Hi")
elif code in ("1001", "1004"):
raise RuntimeError(f"Auth failed: {body['message']}") from e
else:
raise

The full code list is at Error catalogue.

import os, base64, hmac
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
EXPECTED_USER = os.environ["LISOLOO_WEBHOOK_USER"]
EXPECTED_PASS = os.environ["LISOLOO_WEBHOOK_PASS"]
seen: set[str] = set() # in production: Redis / Postgres
@app.post("/lisoloo/webhook")
async def webhook(req: Request, authorization: str | None = Header(None)):
if not authorization or not authorization.startswith("Basic "):
raise HTTPException(401)
raw = base64.b64decode(authorization.split(" ", 1)[1]).decode()
user, _, password = raw.partition(":")
if not (hmac.compare_digest(user, EXPECTED_USER)
and hmac.compare_digest(password, EXPECTED_PASS)):
raise HTTPException(401)
event = await req.json()
if event["event_id"] in seen:
return {"ok": True} # idempotent replay
seen.add(event["event_id"])
# … your business logic …
return {"ok": True}