Skip to content

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

bash
pip install poststack

2. Initialize the client

python
from poststack import PostStack

ps = PostStack(api_key="sk_live_...")

3. 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())

4. Handle errors

Python idioms for error handling, retries, and structured logging when calling the PostStack API.

python
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)
    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 — 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.