Metadata-Version: 2.4
Name: engagelab-otp
Version: 1.0.1
Summary: Official Python SDK for EngageLab OTP — send, verify, and parse webhook callbacks
Author: EngageLab
License: MIT
Project-URL: Homepage, https://www.engagelab.com
Project-URL: Documentation, https://www.engagelab.com/docs/OTP/Product-Overview
Project-URL: Repository, https://github.com/devengagelab/engagelab-otp-python
Keywords: engagelab,otp,sms,verification,webhook,two-factor,2fa
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# engagelab-otp · Python

Official Python SDK for [EngageLab](https://www.engagelab.com) OTP.  
Zero dependencies. Python 3.8+.

## Install

```bash
pip install engagelab-otp
```

## Quick start

```python
import os
from engagelab_otp import OTPClient

otp = OTPClient(
    os.environ["ENGAGELAB_DEV_KEY"],
    os.environ["ENGAGELAB_DEV_SECRET"],
)

# Platform-generated OTP — easiest path
result = otp.send("+6591234567", "your-template-id", {"name": "Alice"}, language="en")
check  = otp.verify(result["message_id"], user_typed_code)
if check["verified"]:
    ...
```

## Two send modes

| Mode               | Method               | When to use |
|--------------------|----------------------|-------------|
| Platform-generated | `otp.send()`         | EngageLab generates and stores the code. You only call `verify()` later. |
| Caller-generated   | `otp.send_custom()`  | You generate the code yourself. EngageLab is just the carrier. |

```python
# Platform-generated
r = otp.send("+6591234567", "tpl-id", {"name": "Alice"}, language="en")
v = otp.verify(r["message_id"], "123456")

# Caller-generated
otp.send_custom("+6591234567", "custom-tpl", {"code": "482910", "name": "Alice"})
```

## Webhook callbacks

EngageLab signs callbacks with `X-CALLBACK-ID` (HMAC-SHA256). The SDK verifies signatures and parses events for you.

> Whitelist source IPs in your firewall: `119.8.170.74`, `114.119.180.30`

```python
from flask import Flask
from engagelab_otp import WebhookVerifier, MessageStatusEvent

app = Flask(__name__)

verifier = WebhookVerifier(
    username=os.environ["ENGAGELAB_WEBHOOK_USERNAME"],
    secret=os.environ["ENGAGELAB_WEBHOOK_SECRET"],
)

def handle(events):
    for e in events:
        if not isinstance(e, MessageStatusEvent):
            continue
        if not e.is_terminal:
            continue   # mid-flight, wait
        if e.status == "delivered":
            mark_delivered(e.message_id)
        elif e.status == "verified":
            mark_verified(e.message_id)

app.add_url_rule(
    "/webhook",
    "engagelab_webhook",
    verifier.flask_view(handle),
    methods=["POST"],
)
```

## Event types

`parse_events()` returns instances of four dataclasses:

| Class                | When                                 | Key fields |
|----------------------|--------------------------------------|------------|
| `MessageStatusEvent` | per-message lifecycle                | `message_id`, `status`, `is_terminal`, `current_send_channel`, `error_code` |
| `NotificationEvent`  | account-level alert                  | `event`, `data` |
| `UplinkEvent`        | inbound user reply                   | `data["from"]`, `data["body"]` |
| `SystemEvent`        | console action audit                 | `event`, `data` |

### Message status enum

```
plan · target_valid · target_invalid
sent · sent_failed
delivered · delivered_failed
verified · verified_failed · verified_timeout
```

`is_terminal == True` for: `delivered`, `delivered_failed`, `sent_failed`, `verified*`, `target_invalid`.

## Error handling

```python
from engagelab_otp import EngagelabError

try:
    otp.send("+6591234567", "tpl", {})
except EngagelabError as e:
    if e.retryable:
        # HTTP 429/5xx, or API codes 1000/5001/5016
        # → exponential backoff
        ...
    else:
        # Permanent failure — fix call or notify user
        print(e.code, e.http_status, str(e))
```

## Examples

See [`examples/`](./examples) for runnable code:

- `01_send_and_verify.py` — platform-generated OTP, full flow
- `02_send_custom.py` — caller-generated code, single + bulk
- `03_webhook_flask.py` — receive callbacks via Flask
- `04_error_handling.py` — retry strategy and error categorization

## Run tests

```bash
python tests/test_sdk.py
```

## License

MIT
