Skip to main content

cloudme-notify

Official Python SDK for the CloudMe Notify multi-channel notification API — SMS, Telegram, WhatsApp, Voice, Email and Push through one client, plus OTP, debt collection and scheduled messages.

Request signing, token exchange and refresh, retries, idempotency and error mapping are handled for you — you never implement Notify's security protocol yourself.

Using Django? Install cloudme-notify-django on top of this package.

Requirements

  • Python 3.9+
  • Your Notify client_id, api_key and RSA private key (generated when you created an API client in the Notify dashboard -> API clients)

Installation

pip install cloudme-notify

Quick start

import os
from cloudme_notify import NotifyClient

notify = NotifyClient(
    client_id=os.environ["NOTIFY_CLIENT_ID"],
    api_key=os.environ["NOTIFY_API_KEY"],
    private_key="/secure/path/private.pem",   # a file path, or the PEM text itself
    base_url=NotifyClient.SANDBOX_URL,        # NotifyClient.PRODUCTION_URL for real sends
    locale="uz",                              # optional: language of error messages - uz, uz-Cyrl, ru, en
)

response = notify.sms.send(to="+998901234567", message="Buyurtmangiz tayyor")
response.message_id   # "0f6c1a52-..."
response.status       # "queued"

The first call signs your credentials and exchanges them for an access token; the token is cached and refreshed automatically from then on.

Sandbox

A sandbox API client (nk_test_... key) works only against NotifyClient.SANDBOX_URL, a production one only against NotifyClient.PRODUCTION_URL (ENVIRONMENT_MISMATCH otherwise). Sandbox sends never reach a real provider and never charge your balance, but return a real message_id, status and price; response.environment is "sandbox".

Sending messages

Every channel has the same send():

notify.sms.send(to="+998901234567", message="Salom!", sms_type="service")
notify.telegram.send(to="+998901234567", message="<b>Tayyor</b>", photo_url="https://example.com/a.jpg")
notify.whatsapp.send(to="+998901234567", message="*Tayyor*")
notify.email.send(to="user@example.com", subject="Hisob-faktura", message="...")
notify.push.send(to="+998901234567", subject="Yangi xabar", message="...")
notify.voice.call(to="+998901234567", message="Buyurtmangiz tayyor")

# with a template instead of free text
notify.sms.send(to="+998901234567", template_id=42, variables={"name": "Aziz"})

# a channel chosen at runtime
notify.channel("sms").send(to="+998901234567", message="...")
  • Free text that matches none of your templates waits for moderation: response.is_held_for_moderation(). Send OTP codes and urgent messages with a template or the OTP API.
  • Telegram supports HTML (<b>, <i>, <u>, <a href>); WhatsApp uses *bold*, _italic_, ~strike~. photo_url (Telegram/WhatsApp) turns the text into the photo's caption.
  • channel_account_id=7 sends through one of your own sender profiles (dashboard -> Channels).

Templates

for template in notify.templates.list(channel="sms"):
    print(template.id, template.name, template.variables)

Telegram cannot message a phone number directly. Send the customer the binding link once; after they press Start, Telegram messages to that phone are delivered:

binding = notify.telegram.binding_link("+998901234567")
binding.bound   # True once linked
binding.link    # "https://t.me/notify_bot?start=..." - send it via SMS/email

Push devices

notify.push.register_device(phone="+998901234567", fcm_token=token_from_your_app)

Idempotency

Every send carries an Idempotency-Key (generated if you don't pass one), so the automatic retry after a network failure never sends twice. Pass your own to make retries across processes safe too:

notify.sms.send(to="+998901234567", message="...", idempotency_key=f"order-{order.id}")

One-time codes (OTP)

Notify generates, delivers and checks the code — your app never sees or stores it:

otp = notify.otp.send(to="+998901234567")          # channel="sms" | "telegram" | "whatsapp" | "email"
# keep otp.otp_id (e.g. in the session)

result = notify.otp.verify(otp.otp_id, code_from_user)
result.verified       # True / False
result.error_code     # OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
result.can_retry()    # only a wrong code with attempts left

A wrong code is a result, not an exception. Sending too soon raises RateLimitError (retry_after_seconds). Neither call is retried automatically. In sandbox otp.code contains the code for testing.

Scheduled messages

Send later — once, or on a recurring timetable — then edit, pause, resume or cancel. These are the same schedules as the dashboard's Scheduled messages page. Requires the schedules:manage scope plus the channel's send scope.

from datetime import datetime
from zoneinfo import ZoneInfo
from cloudme_notify import Recurrence

schedule = notify.scheduled_messages.create(
    channel="sms",
    starts_at=datetime(2026, 10, 6, 10, 0, tzinfo=ZoneInfo("Asia/Tashkent")),  # or "2026-10-06T10:00:00+05:00"
    recipient="+998901234567",
    message="Eslatma: bugun soat 15:00 da uchrashuv",
)

notify.scheduled_messages.create(
    channel="telegram",
    starts_at="2026-10-05T09:00:00+05:00",
    contact_group_id=12,                      # dashboard -> Contacts -> Groups -> API ID (production only)
    message="Salom, {{name}}! Haftalik yangiliklar...",
    recurrence=Recurrence.weekly([1, 4]),     # Recurrence.every(2, "hours"), Recurrence.monthly([1, 15])
)

notify.scheduled_messages.update(schedule.id, channel="sms", starts_at="2026-10-07T10:00:00+05:00",
                                 recipient="+998901234567", message="Uchrashuv ertaga")   # replaces everything
notify.scheduled_messages.pause(schedule.id)
notify.scheduled_messages.resume(schedule.id)    # continues at the next occurrence; missed ones are skipped
notify.scheduled_messages.cancel(schedule.id)
notify.scheduled_messages.find(schedule.id).last_error
notify.scheduled_messages.list(status="active").items

Errors: ValidationError with error_code SCHEDULE_STATE_INVALID, SCHEDULE_START_PASSED, TEMPLATE_NOT_FOUND, CONTACT_GROUP_NOT_FOUND or SANDBOX_GROUP_UNSUPPORTED. A naive datetime (or a string without offset) is read as Asia/Tashkent time.

Debt collection

Hand a debt over and Notify reminds the debtor channel by channel following a collection strategy until you report it paid. Requires the debts:manage scope.

from datetime import date

debt = notify.debts.create(
    name="Aziz Karimov",
    phone="+998901234567",
    amount=1250000,
    due_date=date(2026, 10, 15),
    external_id="INV-1001",       # your reference - makes create() idempotent
)

result = notify.debts.record_payment(debt.id, 1250000, external_id="PAY-5001")
result.debt.is_paid()                 # True -> reminders stop
result.payment.attributed_channel     # "sms" - the reminder that got them to pay

notify.debts.find(debt.id).payments
notify.debts.cancel(debt.id)

Receiving webhooks

Register a webhook per API client (dashboard -> API clients -> Webhook) and verify every request with its signing secret. Always pass the raw body:

from cloudme_notify import InvalidWebhookSignatureError, WebhookVerifier

verifier = WebhookVerifier(os.environ["NOTIFY_WEBHOOK_SECRET"])

try:
    event = verifier.parse(raw_body_bytes, request_headers)
except InvalidWebhookSignatureError:
    ...  # respond 401

if event.event == "message.failed":
    mark_failed(event.message_id, event.error_code)
# a retried delivery carries the same event.id - dedupe on it; respond 2xx within 5 seconds

Status, balance and reports

notify.message(response.message_id).status        # "delivered"
notify.balance().balance                            # "1000000.00"
notify.reports.daily(date(2026, 10, 1), date(2026, 10, 7))     # [{"date", "channel", "count", "amount"}, ...]
notify.reports.monthly()

Error handling

Exception When
ValidationError 422 - error_code, field errors
InsufficientBalanceError / SpendingLimitExceededError 402
ForbiddenError 403 - required_scope on SCOPE_FORBIDDEN
NotFoundError 404
RateLimitError 429 - retry_after_seconds
AuthenticationError 401 that survived one automatic re-authentication
ServerError / NetworkError 5xx / unreachable, after retries
ConfigurationError bad local setup (missing credentials, unreadable key)

All API errors derive from NotifyError and expose error_code, status_code, hint, docs_url and response_body:

from cloudme_notify import NotifyError

try:
    notify.sms.send(to="+998901234567", message="...")
except NotifyError as error:
    log.warning("%s: %s (%s)", error.error_code, error, error.hint)

Configuration

NotifyClient(
    ...,
    token_storage=FileTokenStorage("/var/cache/notify/tokens.json"),  # reuse tokens across runs
    timeout=10.0, connect_timeout=5.0, max_retries=3,
)

The default MemoryTokenStorage lives as long as the client object. Implement get(), put(tokens) and clear() over Redis or your database to share tokens between processes (the Django package does this with Django's cache). NotifyClient can be used as a context manager to close its HTTP connections.

Testing your code

Pass an httpx.MockTransport as transport= to answer requests in-process — no network calls.

License

MIT

Metadata

Release files for cloudme-notify 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cloudme-notify 1.0.0
File Size Uploaded
cloudme_notify-1.0.0.tar.gz 32.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cloudme-notify 1.0.0
File Interpreter ABI Platform
cloudme_notify-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 59.3 kB

Release files / cloudme_notify-1.0.0.tar.gz

Download URL cloudme_notify-1.0.0.tar.gz
Size 32.1 kB
Tags Source
SHA-256 checksum
How to use checksums
92d867551ee5954c2fd3eaf847a88f597392ea0ce10054bf883d712987c5e0a5
BLAKE2b-256 checksum
How to use checksums
47da0ce9949369ee03ea1c3b9a5d830024f19b389d0a1285a3f6cb13ccc13af2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

Release files / cloudme_notify-1.0.0-py3-none-any.whl

Download URL cloudme_notify-1.0.0-py3-none-any.whl
Size 27.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
50a407dbe4ea3fcd21f13b4789e97cd1491782d7b7cbb11979b362204eb24dc8
BLAKE2b-256 checksum
How to use checksums
cd3f5cbd1072e657005f6b00aedf6306da3d761d44d402dd7c421fc5cadab5d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.2

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page