Metadata-Version: 2.4
Name: ownsms
Version: 0.3.7
Summary: Send SMS through your own phone's SIM via API — Django app.
Author-email: ownsms <dev@ownsms.uz>
License-Expression: MIT
Project-URL: Homepage, https://ownsms.omadli.uz
Project-URL: Documentation, https://sms.omadli.uz/api/v1/docs
Project-URL: Repository, https://github.com/ownsms/django-ownsms
Project-URL: Changelog, https://github.com/ownsms/django-ownsms/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/ownsms/django-ownsms/issues
Keywords: sms,bulk-sms,campaign,rassilka,sms-gateway,django,gateway,otp,api,eskiz,playmobile,uzbekistan
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Communications
Classifier: Topic :: Communications :: Telephony
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django<6.0,>=4.2
Requires-Dist: phonenumbers>=8.13
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-django>=4.8; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: tox>=4; extra == "dev"
Requires-Dist: coverage; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: hypothesis; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: django-stubs[compatible-mypy]; extra == "dev"
Requires-Dist: bandit; extra == "dev"
Requires-Dist: pip-audit; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# ownsms

[![PyPI](https://img.shields.io/pypi/v/ownsms)](https://pypi.org/project/ownsms/)
[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue.svg)](https://pypi.org/project/ownsms/)
[![Django](https://img.shields.io/badge/django-4.2%20%7C%205.0%20%7C%205.1%20%7C%205.2-0C4B33.svg)](https://www.djangoproject.com/)
[![CI](https://github.com/ownsms/django-ownsms/actions/workflows/ci.yml/badge.svg)](https://github.com/ownsms/django-ownsms/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/ownsms/django-ownsms/blob/main/LICENSE)

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

```bash
pip install ownsms
```

## Usage

Add the app to `INSTALLED_APPS` and include its URLs:

```python
# 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:

```console
$ python manage.py migrate
```

```http
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](https://github.com/ownsms/django-ownsms/blob/main/QUICKSTART.md) 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:

```bash
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"}}
    ]
  }'
```

```json
{ "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](https://github.com/ownsms/django-ownsms/blob/main/QUICKSTART.md) for the full flow.

## Features

- **Single send** — immediate or `queued`, with per-message `from` (SIM), `ttl`, `idempotency_key`,
  `callback_url`, and `send_at` scheduling.
- **Campaigns** — one template with `{var}` placeholders + a recipients list, fail-fast validation,
  `idempotency_key` for 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_test` API 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:

```bash
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:

```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:

```python
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

```bash
pip install -e ".[dev]"
pytest                 # tests
ruff check src tests   # lint
ruff format src tests  # format
tox                    # full Python × Django matrix
```

## License

[MIT](https://github.com/ownsms/django-ownsms/blob/main/LICENSE)
