Envello
Python guide

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
Free tool

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 →