Send email with Python
Send transactional email from Python through PostStack.
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. Payloads are plain dicts with the same snake_case keys as the REST API (from, to, reply_to, scheduled_at), and responses come back as dicts — result["id"] is the em_… email id.
Quickstart#
Install the SDK
bashpip install poststackInitialize the client
pythonfrom poststack import PostStack ps = PostStack(api_key="sk_live_...")Send an email
python# 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())Handle errors
pythonfrom 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) raise
Framework 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; if a key leaks, revoke it in the dashboard and create a new one.
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.
Ready to send from Python?
Create a free account and an API key — no card needed.