Send emails with Python
Learn how to send transactional emails using PostStack and Python.
The PostStack Python SDK is a thin, typed wrapper around the REST API. It exposes a synchronous client (`PostStack`) for scripts and Django views, and an asynchronous client (`AsyncPostStack`) for FastAPI, asyncio workers, and Celery tasks. Both clients share identical method names and arguments, so you can switch idioms without re-learning the API. The SDK ships full type stubs, so mypy, pyright, and Pylance autocomplete every field of the email payload and every response key.
1. Install the SDK
pip install poststack2. Initialize the client
from poststack import PostStack
ps = PostStack(api_key="sk_live_...")3. Send an email
# Synchronous client
from poststack import PostStack, PostStackError
ps = PostStack(api_key="sk_live_...")
try:
result = ps.emails.send({
"from": "hello@yourdomain.com",
"to": ["user@example.com"],
"subject": "Hello from Python!",
"html": "<h1>Welcome!</h1>",
})
print(result["id"])
except PostStackError as e:
print(f"API error {e.status_code}: {e.error}")
# Asynchronous client
import asyncio
from poststack import AsyncPostStack
async def main():
async with AsyncPostStack(api_key="sk_live_...") as ps:
result = await ps.emails.send({
"from": "hello@yourdomain.com",
"to": ["user@example.com"],
"subject": "Hello from async Python!",
"html": "<h1>Welcome!</h1>",
})
print(result["id"])
asyncio.run(main())4. Handle errors
Python idioms for error handling, retries, and structured logging when calling the PostStack API.
from poststack import PostStack, PostStackError
import logging
logger = logging.getLogger(__name__)
ps = PostStack(api_key="sk_live_...")
# The SDK already retries transient failures (408, 429, and 5xx) up to three
# times with backoff, so application code only needs to handle terminal errors.
try:
result = ps.emails.send({
"from": "hello@yourdomain.com",
"to": ["user@example.com"],
"subject": "Receipt",
"html": "<p>Thanks for your order.</p>",
})
except PostStackError as e:
# Attributes: e.status_code, e.error (message), e.code, e.request_id
if e.status_code == 429:
logger.warning("Rate limited after retries — back off and re-queue (req %s)", e.request_id)
elif 400 <= e.status_code < 500:
logger.error("Invalid request %s: %s (req %s)", e.code, e.error, e.request_id)
else:
logger.error("PostStack server error: %s (req %s)", e.error, e.request_id)
raiseFramework integrations
Django
Add the SDK call to a service module imported from your views. For high-throughput sites, dispatch the send to a Celery task so the request returns immediately. Store the returned `email.id` on your model so you can correlate webhook events later. Use `EMAIL_BACKEND` and Django’s mail framework only if you need Django Admin emails — for transactional flows the SDK is more ergonomic.
FastAPI
Use the `AsyncPostStack` client and `await ps.emails.send(...)` directly inside your async route handlers. The SDK uses `httpx` under the hood and shares an HTTP connection pool, which keeps tail latency low. For background workers, the `BackgroundTasks` system is fine for low-volume sends; for high volume, use Celery, arq, or RQ.
Flask
Initialise the synchronous `PostStack` client once during app factory setup and store it on `current_app.extensions["poststack"]`. Inside views, call `current_app.extensions["poststack"].emails.send(...)`. For high-volume endpoints, push the send into RQ or Celery so the request response is fast.
Celery / RQ background tasks
Wrap `ps.emails.send` in a task and let the task framework own durable retries. The SDK already retries transient API failures (429 and 5xx) with backoff, so the task body only needs to catch `PostStackError` and re-queue on a terminal 5xx. Keep API keys out of task arguments — read them from environment variables inside the task body.
Common pitfalls
Using a sync client inside an async route
Calling the synchronous `PostStack` client from a FastAPI `async def` blocks the event loop and tanks throughput. Always reach for `AsyncPostStack` in async code paths, or wrap sync calls in `asyncio.to_thread`.
Forgetting to await async methods
`AsyncPostStack` returns coroutines; forgetting `await` returns the coroutine object itself and the API call never fires. Enable mypy strict mode to catch this at type-check time.
Hardcoding API keys in source
Read keys from `os.environ` and load them from a `.env` file via `python-dotenv` (or your deployment platform’s secret store). Never commit `sk_live_…` to version control — PostStack auto-revokes leaked keys that hit public GitHub.
Notes
- Official Python SDK — wraps the REST API with sync (PostStack) and async (AsyncPostStack) clients
- Requires Python 3.10+
FAQ
Does the Python SDK work with FastAPI?
Yes — use the `AsyncPostStack` client. Initialise it once at startup and inject it into your route handlers, or use it inside `async def` handlers directly. Connection pooling is handled by the underlying httpx client.
Can I send from a Django view?
Yes. Use the synchronous `PostStack` client for simple cases. For high-traffic views, dispatch the send to a Celery task so the user-facing request returns quickly and the email goes out in the background.
How do I handle rate limits?
The SDK retries rate-limited (429) and server (5xx) responses automatically — up to three attempts with backoff. If it still fails it raises `PostStackError` with `status_code == 429`; catch that and re-queue the send on your own job system. There is no separate `RateLimitError` class — every API failure is a `PostStackError` exposing `status_code`, `error`, `code`, and `request_id`.
Is there a sync client?
Yes. `PostStack(api_key=...)` is fully synchronous. `AsyncPostStack` is the async equivalent. The two share the same method signatures, so you can switch idioms with no code changes beyond `await`.
Related guides
Ready to send emails with Python?
Create a free account and get your API key in under a minute.