A typed Python client for the 1sms.az SMS API, with both synchronous and asynchronous interfaces.
Documentation: https://martian56.github.io/onesms-sdk/
1sms.az API reference: https://1sms.az/api-docs
- Sync (
Client) and async (AsyncClient) clients with the same surface. - OTP, notification, and advertising sends, plus balance and delivery-status lookups.
- Fully typed responses as frozen dataclasses; ships a
py.typedmarker. - Automatic retries with exponential backoff and jitter, honoring
Retry-After. - Idempotency-key support so retried sends are never duplicated.
- A precise exception hierarchy mapped from the API's HTTP status and error codes.
- Webhook signature verification (HMAC-SHA256) with timestamp tolerance.
- No hard dependencies beyond
httpx.
- Python 3.10 or newer
- A 1sms.az API key
Install directly from the repository:
pip install git+https://github.com/martian56/onesms-sdk.gitOr add it to a uv project:
uv add git+https://github.com/martian56/onesms-sdk.gitfrom onesms import Client
with Client("1sk_your_api_key", sender_name="YourSender") as client:
result = client.send_otp("994501234567", "Your code is 123456")
print(result.message_id, result.cost, result.balance)import asyncio
from onesms import AsyncClient
async def main() -> None:
async with AsyncClient("1sk_your_api_key") as client:
result = await client.send_otp("994501234567", "Your code is 123456")
print(result.message_id)
asyncio.run(main())balance = client.balance()
print(balance.balance)
print(balance.apis.otp, balance.apis.bulk, balance.apis.advertising)result = client.send_otp("994501234567", "Your code is 123456")A single recipient returns per-message ids; two or more recipients are dispatched
as a bulk task and return a task_id you can poll.
single = client.send_notification(["994501234567"], "Order shipped")
print(single.message_ids)
bulk = client.send_notification(
["994501234567", "994502223344", "994553334455"],
"Weekend promotion",
)
print(bulk.task_id, bulk.sent_count, bulk.failed_count)
for item in bulk.rejected:
print(item.number, item.reason)result = client.send_advertising(["994501234567"], "Big discounts this week")from onesms import DeliveryStatus
status = client.message_status("message-id")
print(status.status_code, status.status_text)
if status.is_final:
if status.status is DeliveryStatus.DELIVERED:
print("delivered")
else:
print("not delivered:", status.status_text)from onesms import Channel
task = client.task_status("task-id", channel=Channel.NOTIFICATION)
print(task.sent_count, task.failed_count)
for message in task.messages:
print(message.phone, message.status_text, message.is_final)Pass an idempotency_key to make a send safe to retry. The API remembers the key
for 24 hours and will not send the same request twice. When a key is supplied, the
client also retries transient network and server errors automatically.
client.send_otp(
"994501234567",
"Your code is 123456",
idempotency_key="order-4821-otp",
)GET requests and 429 Too Many Requests responses are always retried. Network
failures and 5x responses on writes are retried only when an idempotency key is
present, so a send is never silently duplicated. Backoff is exponential with jitter
and respects a Retry-After header when the API provides one. Configure the ceiling
with max_retries:
client = Client("1sk_your_api_key", max_retries=5, timeout=15.0)Every API failure raises a subclass of OneSmsError.
from onesms import (
Client,
InsufficientBalanceError,
OneSmsAPIError,
OneSmsConnectionError,
OneSmsValidationError,
RateLimitError,
)
try:
client.send_notification(["994501234567"], "Hello")
except OneSmsValidationError as exc:
print("invalid input:", exc)
except InsufficientBalanceError as exc:
print("top up:", exc.required, "have:", exc.balance)
except RateLimitError as exc:
print("retry after:", exc.retry_after)
except OneSmsConnectionError as exc:
print("network problem:", exc)
except OneSmsAPIError as exc:
print(exc.status_code, exc.error_code, exc.message)| Exception | Raised for |
|---|---|
OneSmsValidationError |
Client-side checks before a request is sent |
OneSmsConnectionError |
Network failures that could not be retried |
BadRequestError |
400 |
AuthenticationError |
401 |
InsufficientBalanceError |
402 (exposes required, balance) |
PermissionDeniedError |
403 |
NotFoundError |
404 |
ConflictError |
409 |
RateLimitError |
429 (exposes retry_after) |
ServerError |
5xx (exposes msm_errno, msm_err_text, hint) |
OneSmsAPIError |
Base class for any API error |
1sms.az signs delivery webhooks with an HMAC-SHA256 signature over the request timestamp and raw body. Verify it with your API secret before trusting the payload.
from onesms import WebhookEvent, verify_signature
signature = request.headers["X-1sms-Signature"]
timestamp = request.headers["X-1sms-Timestamp"]
raw_body = request.get_data()
if not verify_signature("your_api_secret", timestamp, raw_body, signature):
raise ValueError("invalid signature")
event = WebhookEvent.from_payload(request.get_json())
if event.is_final:
print(event.message_id, event.status_text)verify_signature rejects timestamps outside a five-minute window by default.
Widen it with tolerance_seconds if needed.
uv sync
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run pytest