letterapp (Python)
Official Python client for letter.app - onboarding email drip campaigns for product teams.
pip install letterapp
Requires Python 3.8+. Zero runtime dependencies (standard library only).
Quick start
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:
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:
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().
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":
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/trackare queued and flushed every 100ms or 50 events by a background daemon thread.sendalways goes out immediately. - Retries -
429waitsRetry-After;5xxand network errors back off exponentially with jitter, up tomax_retries(default 3). Asendwithout anidempotency_keyis never retried, since a duplicate email is worse than a failed one. - Idempotent - every ingestion call gets a UUID
message_idso retries are deduplicated server-side;sendtakes your own key. - No dependencies - HTTP over the standard library
urllib.
API
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.
Metadata
Release files for letterapp 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| letterapp-0.2.0.tar.gz | 8.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| letterapp-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 17.3 kB
Release files / letterapp-0.2.0.tar.gz
| Download URL | letterapp-0.2.0.tar.gz |
|---|---|
| Size | 8.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a04a22ffbc65a8ac45346f9b92ea843a5ef2f4899f0723389679553f25bee54
|
|
BLAKE2b-256 checksum How to use checksums |
78005e43c262a121ceff12fbdc95d91ff91dde5c6a82ca1f7787a51b465bedc7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / letterapp-0.2.0-py3-none-any.whl
| Download URL | letterapp-0.2.0-py3-none-any.whl |
|---|---|
| Size | 9.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
82b2bef836466c7b2e274f8174cb23e67408331181f40a903e33c005a4f1e35a
|
|
BLAKE2b-256 checksum How to use checksums |
eabfd89ca908e99c2fde13023dc3ed79afea164d8fd0b594f6faa289f5b01c01
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|