ownsms
Send SMS through your own phone's SIM card via a simple REST API. ownsms is a Django app that
pairs with an Android device; the device long-polls for outbound messages and dispatches them over
its own SIM — like Eskiz or Play Mobile, but from your number, at your tariff, with no third-party
SMS gateway.
- Documentation: https://ownsms.omadli.uz
- Live API demo: https://sms.omadli.uz/api/v1/docs
- Android sender app: https://github.com/ownsms/ownsms-android
Installation
pip install ownsms
Usage
Add the app to INSTALLED_APPS and include its URLs:
# settings.py
INSTALLED_APPS = [
...
"ownsms",
]
# urls.py
from django.urls import include, path
urlpatterns = [
path("", include("ownsms.urls")),
]
Migrate, then send an SMS with a Bearer API key:
$ python manage.py migrate
POST /api/v1/messages
Authorization: Bearer osk_<your-api-key>
Content-Type: application/json
{"to": "+998901234567", "text": "Hello from ownsms!"}
The paired Android device polls the server, picks up the message, and sends it over the SIM. See the quickstart for the full register → key → send → status flow.
Bulk campaigns
Send one {var} template to many recipients in a single request — the priority use case for
rassilka:
curl -X POST https://sms.omadli.uz/api/v1/campaigns \
-H "Authorization: Bearer osk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Salom {name}, chegirmangiz kodi {code}",
"recipients": [
{"to": "+998901234567", "vars": {"name": "Ali", "code": "A1"}},
{"to": "+998901112233", "vars": {"name": "Vali", "code": "B2"}}
]
}'
{ "id": "camp_1", "status": "running", "total": 2 }
Validation is fail-fast: one bad phone or missing {var} rejects the whole campaign with the
offending rows. Track progress at GET /api/v1/campaigns/camp_1, and pause / resume / cancel via
POST /api/v1/campaigns/camp_1/{action}. Pass an idempotency_key to make retries safe. See the
quickstart for the full flow.
Features
- Single send — immediate or
queued, with per-messagefrom(SIM),ttl,idempotency_key,callback_url, andsend_atscheduling. - Campaigns — one template with
{var}placeholders + a recipients list, fail-fast validation,idempotency_keyfor safe retries, progress, and pause / resume / cancel. - Delivery lifecycle —
queued → sending → sent → delivered | failed,expired,canceled; at-most-once (a job left uncertain by a crash is failed, never resent). - Webhooks — HMAC-signed, retried delivery on status transitions (off by default).
- Per-SIM pacing — rate limits, jitter, working hours, and daily quota, synced to the device.
- Security — API-key scopes (
send/read), revocation, IP allowlist, and an audit log. - Sandbox —
is_testAPI keys simulate delivery without touching a device. - Django admin — every model registered for management.
Full endpoint reference: Swagger UI at /api/v1/docs, schema at /api/v1/openapi.yaml.
Compatibility
Tested on Python 3.11 – 3.14 and Django 4.2 – 5.2 (Django 4.2 and 5.0 require Python ≤ 3.12). Requires Python 3.11+.
Running in production
ownsms is zero-config — it runs with no OWNSMS settings — but two background commands must run
on a timer (cron or a systemd timer) every 1–2 minutes:
python manage.py ownsms_housekeeping # expire TTL-passed messages, reclaim dead leases
python manage.py ownsms_webhooks # deliver pending webhooks
Without ownsms_housekeeping, messages left behind by a dead or offline device stay queued
forever instead of expiring or being reclaimed. Without ownsms_webhooks, status webhooks never
fire.
Behind a reverse proxy
ownsms reads the client IP from the X-Real-IP header for the API-key IP allowlist. Your proxy
must set it to the real peer and must not let clients spoof it. With nginx:
proxy_set_header X-Real-IP $remote_addr;
Without this, the allowlist sees the proxy's own address (or a forged value) and rejects legitimate requests.
Configuration
All settings live in a single OWNSMS dict in settings.py; every key is optional. The ones a
self-hoster usually tunes:
OWNSMS = {
"POLL_BATCH_SIZE": 25, # jobs handed to a device per poll
"LEASE_SECONDS": 300, # seconds a handed-out job is held before reclaim
"DEFAULT_TTL_SECONDS": 86400, # drop a queued message after this long
"DEVICE_ONLINE_SECONDS": 60, # device counts as online if seen within this window
}
Keep POLL_BATCH_SIZE and LEASE_SECONDS matched to device pacing: the phone sends jobs one at a
time with anti-spam jitter, so a large batch with a short lease reclaims still-waiting jobs as
failed before the phone can send them.
Development
pip install -e ".[dev]"
pytest # tests
ruff check src tests # lint
ruff format src tests # format
tox # full Python × Django matrix
License
Metadata
Release files for ownsms 0.3.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ownsms-0.3.8.tar.gz | 47.1 kB | With provenance | Details for ownsms-0.3.8.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ownsms-0.3.8-py3-none-any.whl | Python 3 | none | any | Details for ownsms-0.3.8-py3-none-any.whl |
Total release size: 95.3 kB
Release files / ownsms-0.3.8.tar.gz
| Download URL | ownsms-0.3.8.tar.gz |
|---|---|
| Size | 47.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
789882150fb8a273d958d40da6016f7d3d4dec2b438f36631510a5adb64b37c0
|
|
BLAKE2b-256 checksum How to use checksums |
4bccdff3391696f893fb51d7b50621f3cdc3d4d56de01a533d3d539594e6ddeb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.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 Jul 27, 2026.
Transparency logRelease files / ownsms-0.3.8-py3-none-any.whl
| Download URL | ownsms-0.3.8-py3-none-any.whl |
|---|---|
| Size | 48.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d8c1dde50c8ebcee2e0ac555fc1a900c06d2e95b6940c602c6a1062b5028e54
|
|
BLAKE2b-256 checksum How to use checksums |
cbfc639abce26b90287a1ce07f1bb552b05d0d5b116dabd5b92458623fea0f54
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.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 Jul 27, 2026.
Transparency logSecurity for ownsms 0.3.8
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ownsms-0.3.8.tar.gz
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 Jul 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.