Send email with Python
Send transactional email from Python with the envello SDK, a plain httpx call, or smtplib over SMTP. Sync and async, with retries and idempotency.
Updated · 2 min read
Sending a transactional email from Python is one authenticated POST to https://api.envello.dev/emails. The envello package on PyPI wraps it in a small client; httpx or requests do it directly; and smtplib works if your code already speaks SMTP. Framework specifics are in the Django and FastAPI guides.
Before you start
Read the key from the environment at startup rather than hard-coding it, and keep it out of anything that's logged.
- An Envello account with a verified sending domain
- An API key in an environment variable: ENVELLO_API_KEY=env_live_...
- A from address on that domain, e.g. Acme <[email protected]>
Option 1: the envello SDK
pip install envello. The keyword is from_, with a trailing underscore, because from is reserved in Python.
- import os
- from envello import Envello
- client = Envello(api_key=os.environ['ENVELLO_API_KEY'])
- result = client.emails.send(from_='Acme <[email protected]>', to='[email protected]', subject='Welcome!', html='<p>Welcome aboard</p>')
- print(result.id, result.status) # status is 'queued' on success
Option 2: a plain httpx call
Useful when you want no extra dependency beyond the HTTP client you already have. httpx supports both sync and async, which matters inside async frameworks.
- import os, httpx
- response = httpx.post('https://api.envello.dev/emails', headers={'Authorization': f"Bearer {os.environ['ENVELLO_API_KEY']}"}, json={'from': 'Acme <[email protected]>', 'to': '[email protected]', 'subject': 'Welcome!', 'html': '<p>Welcome aboard</p>'}, timeout=10)
- response.raise_for_status()
- Async: async with httpx.AsyncClient() as client: response = await client.post(...) with the same arguments
Option 3: smtplib over SMTP
For scripts and legacy code that already build email.message.EmailMessage objects. The SMTP username can be any non-empty value; the password is your API key.
- import os, smtplib
- from email.message import EmailMessage
- msg = EmailMessage(); msg['From'] = 'Acme <[email protected]>'; msg['To'] = '[email protected]'; msg['Subject'] = 'Welcome!'; msg.set_content('Welcome aboard')
- with smtplib.SMTP('smtp.envello.dev', 587) as smtp: smtp.starttls(); smtp.login('envello', os.environ['ENVELLO_API_KEY']); smtp.send_message(msg)
Don't block the request
A send is a network call, usually fast but not guaranteed to be. In a web app, send from a background job (Celery, RQ, Dramatiq, or FastAPI's BackgroundTasks) so a slow call never delays the user's response. Use a timeout on every request either way.
Retries without duplicate emails
Background jobs retry, and a retried job can send the same email twice. Add an Idempotency-Key header that's stable for that one email, for example f'reset-{user_id}-{token_id}'. If the first attempt went through, the retry won't send a second copy.
Retry on timeouts and 5xx responses. Don't retry 4xx responses: an unverified from domain, a missing html or text, or a bad key will fail the same way every time.
Sending many at once
POST /emails/batch accepts up to 100 emails per request and reports failures per item. Use it for fan-out jobs like digests instead of looping over single sends.
Common errors
Delivery and bounce events show up in the dashboard logs with the raw event payload, including the receiving server's SMTP response, so a message that left Envello but never arrived can be traced there.
- 401: missing or revoked key, or the Authorization header lacks the Bearer prefix
- 403 domain_not_verified: the from domain isn't verified on your account
- 400: neither html nor text was set, or an address is malformed
Check your domain's SPF, DKIM, and DMARC records
Paste in a domain and see what's missing, plus the exact DNS records to fix it. Free, no account needed.
Check your domain →