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
Metadata
Release files for onesms 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| onesms-0.1.1.tar.gz | 79.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| onesms-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 93.9 kB
Release files / onesms-0.1.1.tar.gz
| Download URL | onesms-0.1.1.tar.gz |
|---|---|
| Size | 79.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fff192b9b4f01b5323d95534f352ba91078d1cdc8475cd289828a0e45ee8b41d
|
|
BLAKE2b-256 checksum How to use checksums |
a2c662bafdbefb831f57564b9c83e70114c2b47aa8ec056a6370532c790b73fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 14, 2026.
Transparency logRelease files / onesms-0.1.1-py3-none-any.whl
| Download URL | onesms-0.1.1-py3-none-any.whl |
|---|---|
| Size | 14.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
33865989996610e83d064b4d627fe3e200570878d82ced98228de3d5df76ef6b
|
|
BLAKE2b-256 checksum How to use checksums |
ac62c43cb94e3e4056f1e80998ad3bdbfc3da20a08f7e7d3378b69cbec29643a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 14, 2026.
Transparency log