Skip to content

Send Emails with Flask

Learn how to send transactional emails using PostStack and Flask.

Flask-Mail wraps SMTP, which is fine for contact forms but leaves deliverability, webhooks, and suppression handling to you. For transactional product email, call the PostStack API directly. The idiomatic Flask pattern is to create one `PostStack` client in your app factory, stash it on `app.extensions`, and reach it from views via `current_app` — then offload the actual send to a background worker (RQ or Celery) so request handlers never wait on the network.

1. Install the SDK

bash
pip install poststack

2. Initialize the client

python
# app.py — initialise the client once in your app factory
import os
from flask import Flask
from poststack import PostStack

def create_app():
    app = Flask(__name__)
    # Store the client on app.extensions so views can reach it via current_app
    app.extensions["poststack"] = PostStack(api_key=os.environ["POSTSTACK_API_KEY"])
    return app

3. Send an email

python
# queue.py — one Redis connection + Queue, imported by both web and worker
from redis import Redis
from rq import Queue

queue = Queue(connection=Redis.from_url("redis://localhost:6379"))

# views.py (a blueprint)
from flask import Blueprint, jsonify, request
from .queue import queue
from .tasks import send_welcome_email

bp = Blueprint("auth", __name__)

@bp.post("/signup")
def signup():
    user = create_user(request.form)
    queue.enqueue(send_welcome_email, user.email, user.name)  # response stays fast
    return jsonify(ok=True)

# tasks.py — runs in an RQ worker process, which has NO Flask app context,
# so use a module logger here rather than current_app.
import os
import logging
from poststack import PostStack, PostStackError

logger = logging.getLogger(__name__)

def send_welcome_email(email: str, name: str):
    ps = PostStack(api_key=os.environ["POSTSTACK_API_KEY"])
    try:
        return ps.emails.send({
            "from": "hello@yourdomain.com",
            "to": [email],
            "subject": "Welcome to Acme",
            "html": f"<h1>Welcome, {name}!</h1>",
        })
    except PostStackError as exc:
        logger.error("PostStack send failed: %s (req %s)", exc.error, exc.request_id)
        raise

4. Handle errors

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

python
import os, logging
from poststack import PostStack, PostStackError

logger = logging.getLogger(__name__)
ps = PostStack(api_key=os.environ["POSTSTACK_API_KEY"])

def send(payload):
    # The SDK retries 429/5xx internally; handle only terminal errors here.
    try:
        return ps.emails.send(payload)
    except PostStackError as e:
        if 400 <= e.status_code < 500 and e.status_code != 429:
            logger.error("PostStack %s: %s (req %s)", e.code, e.error, e.request_id)
            return None
        # 429/5xx survived the SDK's own retries — surface it to the worker
        raise

Framework integrations

App factory + extension pattern

Create the client once inside `create_app()` and store it on `app.extensions["poststack"]`. Views call `current_app.extensions["poststack"].emails.send(...)`. This avoids constructing a new client (and a new connection pool) on every request.

RQ / Celery background jobs

Enqueue the send rather than calling it inline. In the worker, construct the client from `os.environ` and add retry-with-backoff around `PostStackError`. The worker process is the right place for the network call, not the request thread.

Blueprints

Keep email-sending helpers in a dedicated module imported by your blueprints, so the same `send_welcome_email` helper is reusable across `auth`, `billing`, and `account` blueprints.

Flask-Mail vs the SDK

Use Flask-Mail only if you specifically need SMTP for a legacy reason. The SDK gives you the REST API directly — webhooks, tracking, idempotency, and per-message IDs that SMTP cannot return.

Common pitfalls

  • Constructing a client per request

    Creating `PostStack(...)` inside every view spins up a fresh HTTP connection pool each time. Build it once in the app factory and reuse it.

  • Sending inline in the request handler

    A blocking `emails.send()` in the view adds API latency to every response. Enqueue to RQ/Celery and return immediately.

  • Reading the key outside app/worker context

    Load `POSTSTACK_API_KEY` from the environment in both the web app and the worker — they are separate processes and each needs the secret injected.

Notes

  • Initialise one `PostStack` client in the app factory; reuse it via `current_app.extensions`
  • Push sends to RQ or Celery so request handlers stay fast

FAQ

Where should I create the PostStack client in a Flask app?

In your app factory, once. Store it on `app.extensions["poststack"]` and access it through `current_app` in views. This reuses one HTTP connection pool instead of building a new client per request.

How do I send email in the background with Flask?

Enqueue the send with RQ or Celery from your view, and do the actual `ps.emails.send(...)` in the worker with retry-on-`PostStackError`. The request handler returns immediately.

Should I use Flask-Mail or the PostStack SDK?

Use the SDK for transactional product email — it gives you the REST API, webhooks, tracking, and per-message IDs. Flask-Mail only makes sense if you are tied to SMTP for a specific reason.

Related guides

Ready to send emails with Flask?

Create a free account and get your API key in under a minute.