Python SDK
letterapp is the official Python client for the ingestion API. It batches
events on a background thread, retries on transient failures, and generates
idempotency keys for you - so the common letter.track(...) call from a
request handler is non-blocking and safe to retry.
If you’d rather call the HTTP API directly, see the Ingestion API.
| Package | letterapp on PyPI |
| License | MIT |
| Runtime | Python 3.8+ |
| Size | Zero runtime dependencies (standard library only) |
Install
pip install letterapp
The package has no runtime dependencies - HTTP goes over the standard
library urllib, threading over threading + queue. Works on any Python
3.8+: Django, Flask, FastAPI, Celery workers, scripts, Lambda.
You’ll need an API key from Dashboard - Settings - API keys before the SDK will do anything useful.
Quick start
import os
from letterapp import Letter
letter = Letter(api_key=os.environ["LETTER_API_KEY"])
letter.identify(
user_id=user.id,
email=user.email,
traits={"name": user.name, "plan": "free"},
)
letter.group(
user_id=user.id,
account_id=workspace.id,
name=workspace.name,
traits={"plan": workspace.plan, "mrr": 49},
)
letter.track(
user_id=user.id,
event="Workspace Created",
properties={"workspace_id": workspace.id},
)
# Required before the process exits on long-running servers.
letter.close()
Create the api_key in Dashboard - Settings - API keys. It’s shown once
on creation and never again - store it somewhere safe.
You can also use the client as a context manager, which flushes on exit:
with Letter(api_key=os.environ["LETTER_API_KEY"]) as letter:
letter.track(user_id=user.id, event="Workspace Created")
Constructor options
| Option | Default | What it does |
|---|---|---|
api_key | - | Required. lt_live_... from Settings. |
base_url | https://api.letter.app | Override for self-hosting. |
flush_at | 50 | Send a batch when this many items are queued. |
flush_interval | 0.1 (seconds) | Send queued items at most this often. |
max_retries | 3 | Retry attempts on 5xx and 429. |
timeout | 10.0 (seconds) | Per-request socket timeout. |
on_error | prints to stderr | Called when a background flush fails. |
Methods
-
identify(user_id, email=, traits=, timezone=, timestamp=, message_id=) -
group(user_id, account_id, name=, traits=, timestamp=, message_id=) -
track(user_id, event, properties=, timestamp=, message_id=)These enqueue and return immediately. Transport errors surface via
on_error. Fast path for long-running servers. -
flush()- send everything queued now; blocks until the request settles. -
close()- flush, stop the background thread, refuse new enqueues. Runs automatically at interpreter exit, but call it explicitly beforesys.exit()so no events are lost.
Retry behavior
429: waitRetry-Afterseconds, then retry (up tomax_retries).5xxor network errors: exponential backoff (0.25s x 2^attempt + jitter).4xxother than 429: raised immediately (viaon_error), no retry.
The SDK auto-generates a UUID message_id per call, so retries dedupe at the
server. See Idempotency for the underlying guarantee.
Serverless mode
In serverless / function environments there’s no background time between
requests to drain the queue, so set flush_at=1 and call flush() at the end
of each handler:
letter = Letter(api_key=os.environ["LETTER_API_KEY"], flush_at=1)
def handler(event, context):
letter.track(user_id=user_id, event="Checkout Started")
letter.flush()
Errors
Configuration errors and non-retryable API responses raise LetterError
(carrying .status and .body), which uses the same shape as the HTTP API -
see Error format. Background transport errors are
passed to on_error instead, since they can’t be raised to the caller.
Versioning
The SDK follows semver. While we’re at 0.x:
- patch (
0.1.0 -> 0.1.1) - bug fixes only. - minor (
0.1.0 -> 0.2.0) - new options, new methods, behavior changes. - major (
0.x -> 1.0.0) - only once the HTTP API and signatures are stable. Until then, pin a minor range (letterapp~=0.1).
Every request sends a User-Agent: letterapp-python/<version> header so we can
spot outdated clients in server logs.