Skip to content
YourMail

Framework guide

Send email from Python and Django

Why use this

There is no Python SDK, and you do not need one: sending is a single JSON POST to https://api.yourmail.dev/v1/emails, and there is no SMTP server to configure. This guide wraps that call in a small client with requests, then puts it behind a real Django view.

For example

Someone signs up through your Django app and gets a welcome email before the response returns.

1. Install

pip install requests

2. A small client

Authentication is a Bearer API key. Pass it in rather than hardcoding it, and keep the explicit timeout: without one, requests will wait on a stalled connection indefinitely. Every error comes back as {"error": {"type", "message", "docs_url"}}, so the client raises one exception carrying the status, the type and any Retry-After.

# yourmail.py
import requests

API_URL = "https://api.yourmail.dev/v1/emails"


class YourMailError(Exception):
    def __init__(self, status, type, message, retry_after=None):
        super().__init__(message)
        self.status = status
        self.type = type
        self.retry_after = retry_after

    @property
    def retryable(self):
        # 5xx is our side failing: worth another attempt. A rate_limited 429 is
        # too, but only when Retry-After is short; the daily caps answer with
        # the seconds until UTC midnight. Everything else is permanent.
        if self.status >= 500:
            return True
        return (
            self.type == "rate_limited"
            and self.retry_after is not None
            and self.retry_after <= 60
        )


def send_email(email, *, api_key):
    response = requests.post(
        API_URL,
        json=email,
        headers={"Authorization": f"Bearer {api_key}"},
        timeout=10,
    )
    if response.ok:
        return response.json()["id"]

    try:
        error = response.json()["error"]
    except (ValueError, KeyError):
        error = {"type": None, "message": response.text}
    retry_after = response.headers.get("Retry-After")
    raise YourMailError(
        response.status_code,
        error.get("type"),
        error.get("message"),
        int(retry_after) if retry_after else None,
    )

3. The shortest working send

Supplying both html and text is worth the extra line: some clients prefer the plain-text part, and its absence is a mild spam signal.

# send.py
import os

from yourmail import send_email

email_id = send_email(
    {
        "from": "Acme <hello@mail.acme.com>",
        "to": "customer@example.com",
        "subject": "Welcome aboard",
        "html": "<p>Thanks for signing up.</p>",
        "text": "Thanks for signing up.",
    },
    api_key=os.environ["YOURMAIL_API_KEY"],
)
print("queued", email_id)

Run it with YOURMAIL_API_KEY=yourmail_… python send.py. A success returns {"id": "…"}. The from address must be on a domain you have verified; until you have one, send from no-reply@yourmail.dev instead, which delivers to your own inbox in test mode.

4. A Django view

Read the key from the environment once, in settings. It is a server-side secret: call the API only from views, management commands and workers, never from JavaScript you ship to the browser.

# settings.py
import os

YOURMAIL_API_KEY = os.environ["YOURMAIL_API_KEY"]
YOURMAIL_FROM = "Acme <hello@mail.acme.com>"

The body uses idempotency_key because the API accepts the snake_case spellings reply_to, idempotency_key and scheduled_at alongside the camelCase ones, so a Python dict can stay idiomatic. tags is an array of {"name", "value"} pairs, not a dict; a dict is a 400. Tags show up when you filter your message history later.

5. Retry by error type, not attempt count

A 400, 401, 403 or 422 is configuration to fix: an invalid field, a bad key, an unverified domain, a suppressed recipient. It will fail the same way every time. A 5xx, a network timeout, or a 429 rate_limited with a short Retry-After is transient. A 429 quota_exceeded carries no Retry-After at all: it is a ceiling, so surface it rather than sleeping on it.

The other half of a safe retry is the idempotency key. A send is only safe to retry with the same key: without one, a retry after a lost response sends twice. Derive it from something stable, such as the user or order id plus the template name, never a fresh random value per attempt. A repeat with a key already used returns the original message's id instead of sending a second copy.

# Don't do this.
for attempt in range(5):
    try:
        send_email(email, api_key=settings.YOURMAIL_API_KEY)
        break
    except Exception:
        # A suppressed recipient fails all five times, and a rate limit fails
        # harder the more you hammer it. Without an idempotency key, a retry
        # after a lost response sends the message twice.
        continue

The full list of error types, and which are safe to retry, is in the errors reference.

Sending a lot at once

Looping over send_email() for a few hundred recipients works but is slow and wasteful. Use the batch endpoint instead: up to 100 messages per request, each with its own recipients and body.

Switching from another provider?

Last updated