Metadata-Version: 2.4
Name: postmx
Version: 0.1.2
Summary: Official PostMX Python SDK
Project-URL: Homepage, https://github.com/postmx/python
Project-URL: Repository, https://github.com/postmx/python
Project-URL: Issues, https://github.com/postmx/python/issues
Author-email: postmx <dev@postmx.co>
License: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.22; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: twine>=5.1; extra == 'dev'
Description-Content-Type: text/markdown

# PostMX Python SDK

Official Python SDK for the [PostMX](https://postmx.co) API.

- Async-first with sync wrapper
- Full type hints (PEP 561)
- Single dependency (`httpx`)
- Automatic retries with exponential backoff
- Webhook signature verification

Requires Python 3.9+.

## Install

```bash
pip install postmx
```

## Quick Start

### Async

```python
from postmx import PostMX

async def main():
    async with PostMX("pmx_live_...") as postmx:
        # Create a temporary inbox
        inbox = await postmx.create_inbox({
            "label": "signup-test",
            "lifecycle_mode": "temporary",
            "ttl_minutes": 15,
            "message_analysis": {
                "mode": "all",
                "recipients": [],
            },
        })
        print(inbox["email_address"])
        print(inbox["message_analysis"]["mode"])

        # List active inboxes
        result = await postmx.list_inboxes()
        print(result["inboxes"])

        # List messages
        result = await postmx.list_messages(inbox["id"])

        # Or list messages by exact recipient email
        recipient_feed = await postmx.list_messages_by_recipient(inbox["email_address"])

        # Get full message detail with OTP extraction
        detail = await postmx.get_message(result["messages"][0]["id"])
        print(detail["otp"])    # "482910"
        print(detail["intent"]) # "login_code"
        print(detail["analysis"]["status"]) # "queued" | "complete" | ...
```

### Sync

```python
from postmx import PostMXSync

postmx = PostMXSync("pmx_live_...")
inbox = postmx.create_inbox({"label": "test", "lifecycle_mode": "temporary"})
print(inbox["email_address"])
```

## API Reference

### `PostMX(api_key, *, base_url, max_retries, timeout)`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `base_url` | `str` | `https://api.postmx.co` | API base URL |
| `max_retries` | `int` | `2` | Max retry attempts on 429/5xx |
| `timeout` | `float` | `30.0` | Request timeout in seconds |

### Methods

```python
await postmx.list_inboxes(*, limit=None, cursor=None)         # → ListInboxesResult
await postmx.create_inbox(params, *, idempotency_key=None)    # → Inbox
await postmx.list_messages(inbox_id, *, limit=None, cursor=None)  # → ListMessagesResult
await postmx.list_messages_by_recipient(recipient_email, *, limit=None, cursor=None)  # → ListMessagesResult
await postmx.get_message(message_id)                          # → MessageDetail
await postmx.create_webhook(params, *, idempotency_key=None)  # → CreateWebhookResult
await postmx.wait_for_message(inbox_id, *, interval=1.0, timeout=60.0)  # → MessageDetail
```

POST methods accept an optional `idempotency_key`. If not provided, one is auto-generated to make retries safe.

## Error Handling

```python
from postmx import PostMXApiError, PostMXNetworkError

try:
    await postmx.get_message("bad_id")
except PostMXApiError as err:
    print(err.status)              # 404
    print(err.code)                # "not_found"
    print(err.request_id)          # "req_abc123"
    print(err.retry_after_seconds) # None or int
except PostMXNetworkError as err:
    print(err.__cause__)           # original httpx error
```

## Webhook Verification

```python
from postmx import verify_webhook_signature, PostMXWebhookVerificationError

# In your webhook handler (e.g., FastAPI)
@app.post("/webhooks/postmx")
async def handle_webhook(request: Request):
    body = await request.body()
    try:
        event = verify_webhook_signature(
            payload=body,
            signature=request.headers["x-postmx-signature"],
            timestamp=request.headers["x-postmx-timestamp"],
            signing_secret=os.environ["POSTMX_WEBHOOK_SECRET"],
        )
        print(event["data"]["message"]["otp"])
        return {"ok": True}
    except PostMXWebhookVerificationError:
        raise HTTPException(400)
```

## License

MIT
