Receipt Relay is the companion template for the DevOps Daily guide to building transactional email with SMTPFast. It sends one receipt through a small FastAPI backend and traces the message from API acceptance to delivery events and verified webhooks.
The project is deliberately small enough to follow in one sitting while still showing the boundaries that matter in a real integration:
- The SMTPFast API key stays on the server.
- Pydantic validates receipt data before a quota-consuming send.
- User-controlled values are escaped in HTML, with a plain-text alternative.
- The browser receives a provider email ID and follows its delivery trace.
- Webhook signatures are verified against the raw request body.
- Tests use an in-memory HTTP transport and never send real email.
Caution
The form sends a real email when valid SMTPFast credentials are configured. Use an inbox you control while working through the guide.
Create a repository from this template in GitHub, or clone it directly. Then work through the setup below from the project root.
- Python 3.11 or later
- An SMTPFast account
- A sending domain verified in SMTPFast
- A named SMTPFast API key
- An inbox you control for the live test
Docker is optional.
Create a virtual environment and install the application with its development dependencies:
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"Create your local configuration:
cp .env.example .env
chmod 600 .envSet these two required values in .env:
SMTPFAST_API_KEY=replace-with-your-smtpfast-api-key
SMTPFAST_FROM_EMAIL=receipts@your-verified-domain.comStart the application:
make devOpen http://localhost:8080, load the example, enter an inbox you control, and send one receipt.
Before running a live send:
- Add a sending domain in SMTPFast.
- Use Connect to Cloudflare if the domain uses Cloudflare DNS, or publish the displayed DNS records manually.
- Wait until SMTPFast marks the domain as verified.
- Create a named API key and copy it into
.env. - Use a
SMTPFAST_FROM_EMAILaddress on the verified domain.
The API key belongs only in server-side configuration. Do not put it in browser JavaScript, screenshots, commits, or container images.
The current API details are documented by SMTPFast:
- The browser submits receipt fields to
POST /api/receipts. - FastAPI validates the values and renders HTML and plain-text bodies.
- The backend calls
POST /emailswith the server-side SMTPFast key. - SMTPFast returns an email ID. This means the request was accepted, not that the recipient server accepted the message.
- The browser polls
GET /api/emails/{email_id}for delivery events. - When configured, signed SMTPFast webhooks are merged into the same trace.
The UI keeps delivery state separate from engagement. An opened event is
shown as Open signal because image proxies and security scanners can request
the tracking pixel without a person reading the email.
| Endpoint | Purpose |
|---|---|
GET / |
Receipt Relay interface |
GET /health |
Non-sending health check |
GET /api/config |
Safe public configuration flags |
POST /api/receipts |
Validate and submit one receipt |
GET /api/emails/{email_id} |
Retrieve the delivery trace |
POST /webhooks/smtpfast |
Receive signed SMTPFast events |
GET /api/docs |
FastAPI OpenAPI interface |
You can submit a receipt without the browser:
curl --request POST http://localhost:8080/api/receipts \
--header 'Content-Type: application/json' \
--data '{
"customer_name": "Ana Petrova",
"recipient": "you@example.com",
"order_id": "ORD-2048",
"product_name": "Production readiness review",
"amount_cents": 14900,
"currency": "EUR"
}'The response contains the SMTPFast email ID:
{
"email_id": "email_abc123",
"status": "queued",
"latency_ms": 48
}SMTPFast needs a public HTTPS URL. Register this standard-format endpoint after deploying the application or exposing it through a trusted development tunnel:
https://your-app.example/webhooks/smtpfast
Subscribe only to events the application uses, for example:
[
"email.sent",
"email.delivered",
"email.delivery_delayed",
"email.bounced",
"email.failed",
"email.suppressed",
"email.opened",
"email.clicked"
]Store the returned signing secret as SMTPFAST_WEBHOOK_SECRET. The endpoint
rejects missing or invalid signatures and never exposes the secret through
/api/config.
The in-memory event store is intentionally tutorial-sized. A production consumer should persist and deduplicate each verified event before returning a successful response.
make checkThis runs Ruff linting, Ruff formatting validation, and the complete pytest suite. Tests mock SMTPFast, so they need no credentials and consume no quota.
docker build -t smtpfast-receipt-relay .
docker run --rm \
--publish 8080:8080 \
--env-file .env \
smtpfast-receipt-relayThe image runs as a non-root user and exposes /health for container and load
balancer checks.
| Variable | Required | Default | Purpose |
|---|---|---|---|
SMTPFAST_API_KEY |
Yes | None | Server-side key for sends and trace reads |
SMTPFAST_FROM_EMAIL |
Yes | None | Sender on a verified SMTPFast domain |
SMTPFAST_BASE_URL |
No | https://smtpfa.st/api/v1 |
SMTPFast API base URL |
SMTPFAST_TIMEOUT_SECONDS |
No | 20 |
Upstream request timeout |
SMTPFAST_WEBHOOK_SECRET |
For webhooks | None | HMAC signing secret for webhook verification |
APP_ACCESS_TOKEN |
No | None | Shared code for a short-lived public demo |
APP_ACCESS_TOKEN deters casual use of a public demo. It is not user identity,
tenant authorization, or production access control.
This repository demonstrates the integration, not a complete checkout system. Before adapting it to production:
- Load trusted order data from your database instead of accepting totals from a browser.
- Add idempotency so retries and double-clicks cannot send duplicate receipts.
- Store the SMTPFast email ID with the business record that caused the send.
- Persist webhook events with a unique constraint on provider event ID.
- Add real user and tenant authorization.
- Apply per-user, per-tenant, and global quotas.
- Define retention and deletion behavior for recipient data.
- Monitor API failures, bounce categories, delivery latency, and webhook retries.
.
├── app/
│ ├── config.py # Environment-backed settings
│ ├── main.py # FastAPI routes, security headers, and webhook verification
│ ├── models.py # Receipt, trace, and webhook validation
│ ├── smtpfast.py # SMTPFast client and receipt renderer
│ ├── trace_store.py # Bounded tutorial event store
│ └── static/ # Browser interface and delivery timeline
├── tests/ # Mocked API, error, trace, and webhook tests
├── .env.example
├── Dockerfile
├── Makefile
└── pyproject.toml
This project is available under the MIT License.