Metadata-Version: 2.4
Name: unitpost
Version: 0.2.0
Summary: Official Unitpost SDK for Python — send email and manage contacts, segments, campaigns, templates, brand kits, domains, suppressions, webhooks, and usage.
Project-URL: Homepage, https://www.unitpost.com
Project-URL: Documentation, https://www.unitpost.com/docs
Project-URL: Repository, https://github.com/unitpostcom/unitpost
Project-URL: Issues, https://github.com/unitpostcom/unitpost/issues
Author: Unitpost
License: MIT
Keywords: api,email,sdk,transactional-email,unitpost
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.8
Requires-Dist: httpx>=0.28.1
Requires-Dist: typing-extensions>=4.16.0
Provides-Extra: dev
Requires-Dist: mypy>=2.3.0; extra == 'dev'
Requires-Dist: pytest>=9.1.1; extra == 'dev'
Requires-Dist: respx>=0.23.1; extra == 'dev'
Description-Content-Type: text/markdown

# unitpost

Official [Unitpost](https://www.unitpost.com) SDK for Python. Send email and manage
contacts, segments, campaigns, templates, domains, suppressions, and webhooks.

## Install

```bash
pip install unitpost
```

Requires Python 3.8+.

## Quick start

```python
from unitpost import Unitpost

unitpost = Unitpost()  # reads UNITPOST_API_KEY (or pass Unitpost("up_..."))

result = unitpost.email.send({
    "from": "Acme <you@yourdomain.com>",  # bare address works too
    "to": "customer@example.com",
    "subject": "Hello",
    "html": "<p>It works!</p>",
})

if result.error:
    print(result.error.code, result.error.message)
else:
    print("sent", result.data["id"])
```

Every method returns a `Result` with `.data` and `.error`. An API-level failure
populates `.error` (a `UnitpostError`) instead of raising.

## Authentication

Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
[unitpost.com → Settings → API keys](https://www.unitpost.com).

## Client surface

| Accessor | Covers |
| --- | --- |
| `emails` | Send, batch, list, get, update, stats, inbound (`received_*`) |
| `contacts` | CRUD, import jobs |
| `contact_fields` | Custom field definitions |
| `segments` | Segments + membership |
| `topics` | Topics + contact topic prefs |
| `campaigns` | Draft, schedule, send, pause/resume |
| `templates` | Template CRUD |
| `brand_kits` | Brand kit |
| `domains` | Domains + verify |
| `webhooks` | Endpoints + `verify()` |
| `api_keys` | API key management |
| `suppressions` | Suppression list |
| `usage` | Billing-period usage snapshot |

## Pagination

```python
for contact in unitpost.contacts.list_all():
    print(contact["email"])
```

## Suppressions

The suppression list holds addresses excluded from every send — single, batch,
and campaign, including cc/bcc — case-insensitively. Hard bounces and spam
complaints are added automatically; you can also add your own:

```python
# Suppress one address, or many in a single call (idempotent — re-adding is a no-op).
unitpost.suppressions.create({"email": "bounced@example.com", "reason": "manual"})
unitpost.suppressions.create({"emails": ["a@example.com", "b@example.com"]})

# Look one up by id or email; list (optionally filter scope); un-suppress.
unitpost.suppressions.get("bounced@example.com")
for s in unitpost.suppressions.list_all():
    print(s["email"], s["reason"])
unitpost.suppressions.delete("bounced@example.com")
```

`reason` accepts `manual`, `import`, or `api` — the engine-written `bounce` and
`complaint` reasons are read-only. Unitpost-wide (`platform`) entries are visible
but can't be removed via the API.

## Verifying webhooks

```python
from unitpost import verify_webhook

event = verify_webhook(
    payload=raw_body,          # the raw request body (str or bytes)
    secret=os.environ["UNITPOST_WEBHOOK_SECRET"],
    headers=request.headers,
)
```

Or from the client (Resend-style alias):

```python
event = client.webhooks.verify(payload=raw_body, secret=secret, headers=request.headers)
```

## Idempotency

`emails.send` and `emails.batch` are safe to retry by default — if you don't pass
`idempotency_key`, the SDK generates one so an automatic retry can't double-send.
Pass your own to dedupe across separate calls:

```python
unitpost.email.send({...}, idempotency_key="...")
unitpost.email.batch([...], idempotency_key="...")
```

> The server dedupes on `(workspace, idempotency key)` for 24h and replays the
> original result instead of re-sending (the email id for a send, the full list
> envelope for a batch).

## Retries

Transient failures are retried automatically: `429`, `5xx`, and transport errors
(timeouts / network) retry up to `max_retries` times (default `2`). A `429`/`503`
`Retry-After` is honored; otherwise exponential backoff with full jitter is used.
Client errors (`401`/`403`/`404`/`409`/`422`) are never retried. Only requests
safe to replay are retried: `GET`s and writes carrying an idempotency key (which
`emails.send` and `emails.batch` always do); any other keyless write is never
auto-retried.

```python
# Defaults shown; set max_retries=0 to disable.
unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
```

## Resources

| | |
| --- | --- |
| Docs + API reference | https://www.unitpost.com/docs |
| Product / SMTP / MCP guides | https://www.unitpost.com/guides |
| OpenAPI JSON | https://www.unitpost.com/api/v1/openapi.json |
| LLM index | https://www.unitpost.com/llms.txt |
| Dashboard / API keys | https://www.unitpost.com |
| Email components (`@unitpost/email`) | https://www.unitpost.com/components |
| Template gallery | https://www.unitpost.com/templates/gallery |
| Playground | https://www.unitpost.com/playground |
| Migration Assistant | https://www.unitpost.com/migration |
| MCP + Agent Skills | https://www.unitpost.com/guides#ai-mcp · https://www.unitpost.com/guides#ai-skills |
| Support | support@unitpost.com |

## License

MIT
