onesms
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
Features
- 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.
Requirements
- Python 3.10 or newer
- A 1sms.az API key
Installation
pip install onesms
Or add it to a uv project:
uv add onesms
To install the latest unreleased code, point at the repository instead:
pip install git+https://github.com/martian56/onesms-sdk.git
Quickstart
from 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)
Async
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())
Usage
Check the balance
balance = client.balance()
print(balance.balance)
print(balance.apis.otp, balance.apis.bulk, balance.apis.advertising)
Send an OTP
result = client.send_otp("994501234567", "Your code is 123456")
Send notifications
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)
Send advertising
result = client.send_advertising(["994501234567"], "Big discounts this week")
Look up delivery status
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)
Poll a bulk task
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)
Idempotency
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",
)
Retries
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)
Error handling
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 |
Webhooks
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.
Development
uv sync
uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run pytest
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file onesms-0.1.0.tar.gz.
File metadata
- Download URL: onesms-0.1.0.tar.gz
- Upload date:
- Size: 79.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e9b03831a6a762c3928658f3bb1a7e6e0860ec9716f229f4b25585bd215f5e20
|
|
| MD5 |
11556cfe211fcc3979fac2c2e440bcea
|
|
| BLAKE2b-256 |
7a484af1cb4ea35b55aa3824a5f2015e091210d0b44093e2012b22e2a5ca2c4d
|
File details
Details for the file onesms-0.1.0-py3-none-any.whl.
File metadata
- Download URL: onesms-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
237825014d34a5e9cc62058ea42b1a63c1870324815e4b73c6566e72a0330220
|
|
| MD5 |
865561816b4f911ee640db85d9c21e2c
|
|
| BLAKE2b-256 |
7b21b06283da286ad965a239601dcbf3945731aaeee9b1f83186e0255d2b3d57
|