A runnable FastAPI app that sends email through MailKite's API — an HTML form for a quick visual test, plus a JSON endpoint for curl or another service.
There's no swappable "mailer" concept in FastAPI itself (unlike Django's
EMAIL_BACKEND or Laravel's Mail::extend) — apps that need email just call
an SDK or an SMTP library directly. This starter shows the idiomatic way to do
that: call the MailKite Python SDK, and keep the (synchronous) SDK call from
blocking FastAPI's async event loop. See
mailkite_client.py for how, and
the integration docs for why.
python -m venv .venv && source .venv/bin/activate # optional but recommended
pip install -r requirements.txt
cp .env.example .env # then edit .env with your real keyRequired environment variables:
| Variable | Required | Description |
|---|---|---|
MAILKITE_API_KEY |
yes | Your MailKite API key (mk_live_…) — dashboard → Settings → API key, or a domain-scoped key per app/tenant. |
MAILKITE_FROM |
no | Prefills the "From" field on the form. Must be an address on a domain you've verified with MailKite. |
Load .env however your shell does (export $(cat .env | xargs), direnv,
python-dotenv, ...) — this starter doesn't hard-code a .env loader so it
stays dependency-minimal; add python-dotenv if you want it auto-loaded.
export MAILKITE_API_KEY=mk_live_your_key_here
uvicorn main:app --reloadOpen http://127.0.0.1:8000/ — fill in the form and send a real email.
Or hit the JSON endpoint directly:
curl -s -X POST http://127.0.0.1:8000/api/send \
-H "Content-Type: application/json" \
-d '{"to":"ada@example.com","subject":"Hi from FastAPI","html":"<p>Hello!</p>"}'Interactive API docs (Swagger UI) are at http://127.0.0.1:8000/docs.
| File | What it does |
|---|---|
main.py |
The FastAPI app: GET / (form), POST /send (form handler), POST /api/send (JSON). |
mailkite_client.py |
Wraps the sync MailKite SDK in run_in_threadpool so sends don't block the event loop. |
templates/index.html |
The send-mail form (Jinja2). |
static/style.css |
Minimal, theme-aware styling — no build step, no frontend framework. |
requirements.txt |
fastapi, uvicorn, jinja2, python-multipart, mailkite-dev. |
mailkite-dev (the official Python SDK) is intentionally zero-dependency and
synchronous — it uses urllib.request under the hood, not httpx/aiohttp.
Calling mk.send(...) inside an async def path operation would block the
whole event loop for the length of that HTTP round trip, stalling every other
concurrent request on that worker. mailkite_client.send_email() wraps the
call in FastAPI/Starlette's run_in_threadpool instead, so the blocking I/O
runs on a worker thread. (A plain def path operation gets this for free —
FastAPI runs sync route handlers in a threadpool automatically — but the
explicit wrap here keeps the route async def and makes the tradeoff visible
rather than relying on that implicit behavior.)
For fire-and-forget sends (e.g. a signup confirmation you don't want the
response to wait on), use BackgroundTasks instead:
from fastapi import BackgroundTasks
@app.post("/signup")
async def signup(background_tasks: BackgroundTasks, ...):
create_user(...)
background_tasks.add_task(get_client().send, {...}) # fire-and-forget
return {"ok": True}MailKite also speaks SMTP (smtp.mailkite.dev:587, STARTTLS, your API key as
the password) — see
the SMTP relay docs. That's a good fit
if you're already using fastapi-mail / fastapi-mailman (both wrap
aiosmtplib) and don't want to touch your send call sites; this starter uses
the API path instead because it gets you templates, batch sends, scheduled
sends, and inbound webhooks that plain SMTP can't express.
Full write-up: https://mailkite.dev/docs/integrations/fastapi