NorthRelayNorthRelay
Guides

Python Integration

Send emails from Python applications using smtplib, the NorthRelay SDK, Django, Flask, or FastAPI

Python Integration

This guide covers sending email from Python using the standard library (smtplib), the northrelay SDK, and popular web frameworks.

Using smtplib (Standard Library)

No additional packages needed. Python's built-in smtplib supports SMTP with STARTTLS.

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

def send_email(to: str, subject: str, text: str, html: str | None = None):
    msg = MIMEMultipart("alternative")
    msg["Subject"] = subject
    msg["From"] = "notifications@yourdomain.com"
    msg["To"] = to

    msg.attach(MIMEText(text, "plain"))
    if html:
        msg.attach(MIMEText(html, "html"))

    with smtplib.SMTP("smtp.north-pipe.ca", 587) as server:
        server.starttls()
        server.login("your-email@example.com", "nr_live_xxxxxxxxxxxx")
        server.send_message(msg)

    print(f"Email sent to {to}")


# Usage
send_email(
    to="recipient@example.com",
    subject="Hello from NorthRelay!",
    text="This is a test email.",
    html="<h1>Hello!</h1><p>This is a test email.</p>",
)

With Environment Variables

import os
import smtplib
from email.mime.text import MIMEText

SMTP_HOST = "smtp.north-pipe.ca"
SMTP_PORT = 587
SMTP_USER = os.environ["NORTHRELAY_USER"]
SMTP_PASS = os.environ["NORTHRELAY_API_KEY"]
FROM_ADDRESS = os.environ.get("NORTHRELAY_FROM", SMTP_USER)

def send_email(to: str, subject: str, body: str):
    msg = MIMEText(body)
    msg["Subject"] = subject
    msg["From"] = FROM_ADDRESS
    msg["To"] = to

    with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
        server.starttls()
        server.login(SMTP_USER, SMTP_PASS)
        server.send_message(msg)

Using the NorthRelay SDK

Installation

pip install northrelay

The SDK is async: every method is a coroutine. Use it from async code (FastAPI, Starlette, aiohttp) or wrap it with asyncio.run.

Basic Usage

import asyncio
import os

from northrelay import NorthRelay, SendEmailRequest


async def main() -> None:
    async with NorthRelay(api_key=os.environ["NORTHRELAY_API_KEY"]) as client:
        result = await client.emails.send(
            SendEmailRequest(
                from_={"email": "notifications@yourdomain.com", "name": "My App"},
                to=[{"email": "recipient@example.com"}],
                content={
                    "subject": "Hello from NorthRelay!",
                    "text": "This is a test email.",
                    "html": "<h1>Hello!</h1><p>This is a test email.</p>",
                },
            )
        )
        print(f"Email queued: {result.message_id} (status: {result.status})")


asyncio.run(main())

async with closes the connection pool on exit; a long-lived client can instead call await client.close() at shutdown.

Sending a Template

result = await client.emails.send_template(
    "template_id",
    [{"email": "alice@example.com", "name": "Alice"}],
    {"first_name": "Alice", "order_id": "1234"},
    from_={"email": "orders@yourdomain.com"},
)

Always pass from_ with an address on a domain you have verified.

Error Handling

from northrelay import NorthRelayError, RateLimitError, ValidationError

try:
    result = await client.emails.send(
        SendEmailRequest(
            from_={"email": "notifications@yourdomain.com"},
            to=[{"email": "recipient@example.com"}],
            content={"subject": "Test", "text": "Test email"},
        )
    )
    print(f"Queued: {result.message_id}")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after} seconds.")
except ValidationError as e:
    print(f"Invalid request: {e.message} {e.errors}")
except NorthRelayError as e:
    print(f"API error [{e.details.get('code')}] ({e.status_code}): {e.message}")
    if e.details.get("fix_action"):
        print(f"Suggested fix: {e.details['fix_action']}")

Every exception extends NorthRelayError and carries message, status_code and details (the API's code, fix_action and docs_url). See the Python SDK for the full list.

Using Django

Django's built-in email framework supports SMTP out of the box.

settings.py

# Email configuration
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.north-pipe.ca"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ["NORTHRELAY_USER"]
EMAIL_HOST_PASSWORD = os.environ["NORTHRELAY_API_KEY"]
DEFAULT_FROM_EMAIL = "notifications@yourdomain.com"

Sending Email

from django.core.mail import send_mail, EmailMultiAlternatives

# Simple text email
send_mail(
    subject="Welcome!",
    message="Thanks for signing up.",
    from_email=None,  # Uses DEFAULT_FROM_EMAIL
    recipient_list=["recipient@example.com"],
)

# HTML email with plain text fallback
email = EmailMultiAlternatives(
    subject="Welcome!",
    body="Thanks for signing up.",
    from_email=None,
    to=["recipient@example.com"],
)
email.attach_alternative("<h1>Welcome!</h1><p>Thanks for signing up.</p>", "text/html")
email.send()

Testing in Django

# settings.py — for local development, log emails to console
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

Using Flask-Mail

Installation

pip install Flask-Mail

Configuration

from flask import Flask
from flask_mail import Mail, Message

app = Flask(__name__)
app.config.update(
    MAIL_SERVER="smtp.north-pipe.ca",
    MAIL_PORT=587,
    MAIL_USE_TLS=True,
    MAIL_USERNAME=os.environ["NORTHRELAY_USER"],
    MAIL_PASSWORD=os.environ["NORTHRELAY_API_KEY"],
    MAIL_DEFAULT_SENDER="notifications@yourdomain.com",
)

mail = Mail(app)

@app.route("/send")
def send():
    msg = Message(
        subject="Hello from Flask!",
        recipients=["recipient@example.com"],
        body="This is a test email from Flask.",
        html="<p>This is a test email from <strong>Flask</strong>.</p>",
    )
    mail.send(msg)
    return "Email sent!"

Using FastAPI with the SDK

Installation

pip install fastapi uvicorn northrelay

Application

import os
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from northrelay import NorthRelay, NorthRelayError, SendEmailRequest

email_client = NorthRelay(api_key=os.environ["NORTHRELAY_API_KEY"])


@asynccontextmanager
async def lifespan(app: FastAPI):
    yield
    await email_client.close()


app = FastAPI(lifespan=lifespan)


class EmailRequest(BaseModel):
    to: EmailStr
    subject: str
    body: str


@app.post("/send-email")
async def send_email(request: EmailRequest):
    try:
        result = await email_client.emails.send(
            SendEmailRequest(
                from_={"email": os.environ["NORTHRELAY_FROM"]},
                to=[{"email": request.to}],
                content={"subject": request.subject, "text": request.body},
            )
        )
        return {"success": True, "messageId": result.message_id}
    except NorthRelayError as e:
        raise HTTPException(status_code=e.status_code or 502, detail=e.details.get("fix_action") or e.message)

Run with:

uvicorn app:app --reload

Environment Variables

Store credentials in environment variables. Never hardcode API keys.

# .env
NORTHRELAY_USER=your-email@example.com
NORTHRELAY_API_KEY=nr_live_xxxxxxxxxxxx
NORTHRELAY_FROM=notifications@yourdomain.com

Load with python-dotenv:

from dotenv import load_dotenv
load_dotenv()

On this page