# Python SDK

> One client for every endpoint. Tokens, idempotency keys and error types are handled for you.

The `ens_sdk` package wraps the API in one client: OAuth tokens,
idempotency keys and clear errors are handled inside, so your code sends
notifications instead of HTTP requests.

### Install
```bash
pip install ens_sdk
```
### Configure
Credentials and tenant come from the environment, or from constructor
arguments, which take precedence:

```bash
ENS_SERVER_URL=https://nofy.encipher.co.tz
ENS_CLIENT_ID=<oauth application client id>
ENS_CLIENT_SECRET=<oauth application client secret>
ENS_TENANT=<carrier tenant external id>
ENS_TIMEOUT=10
```

```python
from ens_sdk import ENS

ens = ENS()  # reads the environment above
# or: ENS(server_url=..., client_id=..., client_secret=..., tenant=...)
```

Tokens are minted with client credentials and cached until they expire.
Calls without configuration raise `ENSRefused` straight away instead of
failing obscurely later.
### Send
```python
ens.send_email("ops@acme.co.tz", "Shift closed", "Shift 7 closed.")
ens.send_whatsapp_template("+255700000000", "otp_login", "en", ["482916"])
ens.send_sms("+255700000000", "Your code is 482916")
ens.send_push("user-uuid-1", "Shift closed", "Tap for the summary.")
```

Email and WhatsApp accept `html` and `inline_images` (email),
`callback_url`, `event_type`, `reference` and `idempotency_key`. These
default to sensible unique values when omitted, so plain calls are already
retry-safe. Push needs a tenant and addresses your user ids, so register
devices first.

!!! note "send_sms uses the legacy path"
    `send_sms` posts to `/v1/notification/send/`, which sends exactly like
    the carrier channel but skips idempotency keys, and attributes quotas
    to a tenant only when you pass `tenant=`. New SMS integrations that need
    those guarantees should call `POST /v2/messages` directly (see
    [Sending on v2](sending-v2)). The [Quickstart](quickstart) shows the
    same send in Node.js, PHP, Go, Rust and cURL.

## Devices & tenants

```python
ens.register_device("user-uuid-1", "fcm-device-token", platform="android")
ens.deregister_device(external_user_id="user-uuid-1")  # logout: whole fleet
ens.provision_tenant("company-uuid-1", name="Acme Fuel")  # idempotent
```

## Errors

| Exception          | Meaning                                              |
|--------------------|------------------------------------------------------|
| `ENSRefused`       | Not configured, missing tenant for the call, or the service answered `400`. |
| `ENSQuotaExceeded` | Daily quota reached (`429`). Back off until tomorrow. |
| `ENSUnreachable`   | Network fault or unexpected status. Safe to retry.   |

All three subclass `ENSError`, so one `except ENSError` covers the client.
