Metadata-Version: 2.4
Name: letterapp
Version: 0.2.0
Summary: Official Python client for letter.app - transactional email and onboarding drip campaigns. Auto-batching, retries, idempotency.
Project-URL: Homepage, https://letter.app/docs/python-sdk
Project-URL: Repository, https://github.com/vincenzor/letter-python
Project-URL: Issues, https://github.com/vincenzor/letter-python/issues
Author: letter.app
License-Expression: MIT
License-File: LICENSE
Keywords: analytics,drip,email,identify,ingestion,letter,letterapp,onboarding,sdk,track
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 :: Only
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# letterapp (Python)

[![PyPI version](https://img.shields.io/pypi/v/letterapp)](https://pypi.org/project/letterapp/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green)](./LICENSE)

Official Python client for **[letter.app](https://letter.app)** - onboarding
email drip campaigns for product teams.

```bash
pip install letterapp
```

Requires Python **3.8+**. Zero runtime dependencies (standard library only).

## Quick start

```python
import os
from letterapp import Letter

letter = Letter(api_key=os.environ["LETTER_API_KEY"])  # Dashboard -> Settings -> API keys

# Tell Letter who your user is (call where users sign up or log in).
letter.identify(
    user_id="user_123",
    email="alice@example.com",
    traits={"name": "Alice", "plan": "free"},
)

# Report something they did.
letter.track(user_id="user_123", event="Signed Up", properties={"source": "web"})

# Required before the process exits so no events are lost.
letter.close()
```

Or use it as a context manager, which flushes on exit:

```python
with Letter(api_key=os.environ["LETTER_API_KEY"]) as letter:
    letter.track(user_id="user_123", event="Workspace Created")
```

## Serverless (Lambda, Cloud Functions)

There is no background time to flush in a serverless handler, so set
`flush_at=1` and `flush()` at the end of each invocation:

```python
letter = Letter(api_key=os.environ["LETTER_API_KEY"], flush_at=1)

def handler(event, context):
    letter.track(user_id="user_123", event="Checkout Started")
    letter.flush()
```

## Transactional email

`send()` mails one person right now: a receipt, a password reset, a
verification link. It is never batched and never waits for `flush()`.

```python
result = letter.send(
    to="alice@example.com",
    subject="Reset your password",
    html="<p>Click <a href='https://...'>here</a> to reset.</p>",
    tag="password-reset",
    idempotency_key=f"password-reset:{token}",
)

result["messageId"]  # provider id, appears in delivery events
```

Only `to`, `subject` and one of `html` / `text` are required. `from_email`
defaults to the project's sender and must be on a verified domain. A
plain-text part is derived from the HTML when you don't supply one.

Pass an `idempotency_key` whenever the call can be retried (a queue worker, a
webhook handler): a replay returns the original send rather than mailing the
recipient twice, and it's what lets the SDK retry a `5xx` safely.

Failures raise `LetterError` with `.status`, `.code` and `.reason`. The reason
is what tells "this recipient is unreachable" apart from "our account is
blocked":

```python
try:
    letter.send(to=to, subject=subject, html=html)
except LetterError as err:
    if err.reason == "suppressed":
        return  # hard-bounced or complained; nothing to fix
    raise
```

Transactional mail ignores marketing unsubscribes (an opted-out user still
gets their password reset) but respects bounces, complaints, and addresses
suppressed by hand.

## What it does

- **Auto-batching** - `identify` / `group` / `track` are queued and flushed
  every 100ms or 50 events by a background daemon thread. `send` always goes
  out immediately.
- **Retries** - `429` waits `Retry-After`; `5xx` and network errors back off
  exponentially with jitter, up to `max_retries` (default 3). A `send` without
  an `idempotency_key` is never retried, since a duplicate email is worse than
  a failed one.
- **Idempotent** - every ingestion call gets a UUID `message_id` so retries are
  deduplicated server-side; `send` takes your own key.
- **No dependencies** - HTTP over the standard library `urllib`.

## API

```python
Letter(
    api_key,
    base_url="https://api.letter.app",  # only set for self-hosted / local
    flush_at=50,                          # 1 for serverless
    flush_interval=0.1,                   # seconds
    max_retries=3,
    timeout=10.0,
    on_error=None,                        # callback(Exception) for bg errors
)

letter.identify(user_id, email=None, traits=None, timezone=None, timestamp=None, message_id=None)
letter.group(user_id, account_id, name=None, traits=None, timestamp=None, message_id=None)
letter.track(user_id, event, properties=None, timestamp=None, message_id=None)
letter.send(to, subject, html=None, text=None, from_email=None, from_name=None,
            reply_to=None, headers=None, tag=None, metadata=None,
            idempotency_key=None)  # -> dict, sent immediately
letter.flush()   # send queued calls now, block until done
letter.close()   # flush + stop the background thread (also runs at exit)
```

Configuration errors and non-retryable API responses raise `LetterError`
(with `.status`, `.code`, `.reason` and `.body`). Background transport errors
are passed to `on_error` instead, since they cannot be raised to the caller.

## Full documentation

- **SDK reference:** <https://letter.app/docs/python-sdk>
- **Ingestion API:** <https://letter.app/docs/api>
- **Transactional API:** <https://letter.app/docs/transactional>

## License

MIT - see [LICENSE](./LICENSE).
