Python
Python 3.9+. Either requests (the
go-to for sync code) or httpx (sync
- async, same surface). Examples in both.
pip install requests# orpip install httpximport 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"}Send an SMS
Section titled “Send an SMS”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"])import httpx
with httpx.Client(timeout=10, headers=HEADERS) as client: r = client.post(f"{API_URL}/send", json={ "to": ["+243998857000"], "message": "Hello from Lisoloo!", "sender_id": "MYAPP", }) r.raise_for_status() print(r.json()["data"]["message_id"])import asyncioimport httpx
async def send(): async with httpx.AsyncClient(timeout=10, headers=HEADERS) as client: r = await client.post(f"{API_URL}/send", json={ "to": ["+243998857000"], "message": "Hello from Lisoloo!", "sender_id": "MYAPP", }) r.raise_for_status() return r.json()["data"]
print(asyncio.run(send())["message_id"])A reusable client class
Section titled “A reusable client class”from dataclasses import dataclassfrom typing import Iterable, Optionalimport requests
@dataclassclass 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 sendresult = client.send(["+243998857000"], "Hello!", sender_id="MYAPP")print(result["message_id"])
# Scheduledclient.send( ["+243998857000"], "Reminder", sending_type="scheduled", scheduled_dates=[{"date": "2026-06-01", "time": "08:00"}],)
# Recurringclient.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"], },)Handling errors
Section titled “Handling errors”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: raiseThe full code list is at Error catalogue.
FastAPI webhook receiver
Section titled “FastAPI webhook receiver”import os, base64, hmacfrom 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}See also
Section titled “See also”- Quickstart — same code, side-by-side with cURL/JS/PHP
- Authentication
- Webhooks configuration — the Basic auth contract