Skip to content

Alievs-corp/onesms-sdk

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

onesms-sdk

CI Docs Python License: MIT

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.typed marker.
  • 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

Install directly from the repository:

pip install git+https://github.com/martian56/onesms-sdk.git

Or add it to a uv project:

uv add 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

MIT

About

Python client for the 1sms.az SMS API, with sync and async interfaces.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Python 100.0%