Python
Updated
Send emails from Python with Lumail
Lumail has no Python package, and it does not need one: sending is a single authenticated POST. This guide wraps it in a small helper you can drop into any script, Django view, Flask route or FastAPI endpoint.
TL;DR
https://lumail.io/api/v2/emails with Authorization: Bearer <LUMAIL_API_KEY> and a body of from, to, subject and exactly one of html, markdown or tiptap. A 2xx response returns {"id": "eml_..."}. Errors return name, message and statusCode. Send an Idempotency-Key header on anything you might retry.Prerequisites
- Python 3.10 or later with
requests(sync) orhttpx(sync and async). - A Lumail API token that starts with
lum_. - A sending domain verified in the same Lumail organization as the token.
1. Install the packages
2. Set your environment variables
Read the token from the environment with os.environ. Locally, export it in your shell or load a .env file with your framework's usual tool; in production, set it in your platform's secret settings.
Reading it with os.environ["LUMAIL_API_KEY"] rather than .get() makes a missing key fail at import time instead of producing 401s later.
3. Write a small send helper
The helper sets the bearer token, adds the optional idempotency header, and turns error responses into an exception that carries Lumail's error name and status.
from is a Python keyword, so the payload is built as a dict rather than with keyword arguments.
4. Send an email
Pass one recipient in to. Leave text out and Lumail builds the plain-text part from your HTML.
5. Send from FastAPI with httpx
In an async framework, use httpx.AsyncClient so the request does not block the event loop. Create the client once with the app lifespan and reuse its connection pool.
6. Send a batch
POST a JSON array of up to 100 email objects to /api/v2/emails/batch. The response is {"data": [{"id": ...}]} in the same order as your array.
Handle errors
Error responses are JSON with a stable name, a human message and the statusCode. Branch on name, log both, and show your user a generic message.
Retry connection errors, timeouts and 5xx responses. Do not retry 4xx responses: a validation error will fail the same way, and a 429 from the recipient guard means that mailbox has already received enough email for now.
| Status | error.name | What to do |
|---|---|---|
| 401 | missing_api_key | Missing or invalid token. Check the env var on the server that sends. |
| 403 | missing_permission | The token lacks the emails permission. Mint one that has it. |
| 402 | plan limit | The organization used its plan's email volume. Upgrade or wait for the next period. |
| 4xx | validation_error | Missing field, more than one body format, or a from domain that is not verified. |
| 429 | RECIPIENT_RATE_LIMITED | That mailbox hit 5 sends in 10 minutes or 20 in 24 hours. Honor Retry-After. |
| none | network_error / timeout | The request never got an answer. Safe to retry with the same idempotency key. |
Make retries safe with idempotency
Send an Idempotency-Key header built from whatever triggered the email, such as welcome:<user id> or a webhook event id. Repeating the key returns the email already queued instead of sending another. Keys can be up to 256 characters.
This is what makes retries safe in Celery, RQ or any job runner that may run a task twice.
Verify your sending domain
Lumail only sends from a domain you have verified. Add the domain in your organization's Domains settings, then publish the SPF, DKIM and DMARC records it shows at your DNS provider. Until the domain verifies, every send fails with an error saying the domain is not authorized or verified.
Use a subdomain such as mail.yourdomain.com if your root domain already sends from another provider. Start DMARC at p=none, then tighten it once reports look clean.
- Email domains - Add a domain and the SPF, DKIM and DMARC records.
- Add a DMARC record - Publish a policy, then tighten it safely.
- Mail tester - Send a real email and check authentication and spam signals.
Production checklist
- Every request sets a timeout. Neither
requestsnor a barehttpxcall should wait forever on a network problem. - Background jobs pass an idempotency key so a task that runs twice sends once.
- The
fromaddress is on a domain verified in the same organization as the token, with SPF, DKIM and a DMARC record. LUMAIL_API_KEYis set on every environment that sends, never committed, never in a public or client-side variable. Use one token per environment.- Every send that can be retried (webhooks, queues, jobs) passes an idempotency key derived from the triggering event.
- One-time codes and magic links set
tracking: { links: false, open: false }so links are not rewritten. - End-to-end tests send to fixture addresses such as
[email protected], which return an id without sending real email. - A failed email is logged with
error.nameand never fails the user's request on its own.
Frequently asked questions
Is there a Lumail Python SDK?
Not today. The official SDK is the lumail package on npm. From Python, call the REST API directly with requests or httpx, which takes about twenty lines including error handling, as shown in this guide.
Should I use requests or httpx?
Use requests in synchronous code such as scripts, Django views and Flask routes. Use httpx.AsyncClient in async frameworks like FastAPI or Starlette so the call does not block the event loop.
Can I use smtplib instead of the API?
Yes. Lumail exposes an SMTP relay at smtp.lumail.io on port 587 with STARTTLS, any non-empty username and your API token as the password. The HTTP API is preferred because it returns clearer errors and supports idempotency keys.
How do I send Markdown instead of HTML from Python?
Pass a markdown field instead of html. Lumail renders it to HTML on send. Provide exactly one of html, markdown or tiptap in each request.
What does a successful response look like?
A 2xx response with a JSON body like {"id": "eml_abc123"}. The email is queued at that point. Fetch GET /api/v2/emails/{id} later to read its last delivery event.
How do I add a subscriber to my list from Python?
POST to /api/v2/subscribers with the same bearer token. It upserts by email and accepts tags and custom fields, which is how a signup in your app enters your marketing workflows.
Keep building
- GuideSend emails from Next.js with LumailApp Router route handlers and server actions with the lumail SDK.
- GuideSend emails from Node.js with LumailA script, an Express route, batch sends and a safe retry helper.
- GuideSend emails from TanStack Start with LumailServer functions and server routes that keep the key on the server.
- DocsSend Email API referenceEvery field, option and error for POST /api/v2/emails.
- DocsBatch Emails API referenceUp to 100 transactional emails in one request.
- DocsSMTP relayFor tools that can only speak SMTP.
- DocsCreate subscriber APIUpsert a contact with tags and custom fields.
- DocsLumail docsAPI reference, SDK and tutorials.
- Free toolMail testerCheck SPF, DKIM, DMARC and spam signals on a real send.
Send your first email from code today.
3,000 emails a month free. Transactional and marketing email on one verified domain, with unlimited subscribers on every plan.