← Back to blog
·7 min read·
Paulo CastellanoPaulo Castellano

How to send email with Python using an API or SMTP

Send email from Python apps using the Sendkit SDK. Covers setup, HTML email, attachments, error handling, and SMTP.

pythonemail-apitransactional-emailtutorial
How to send email with Python using an API or SMTP

Python's built-in smtplib works, but it's a pain. You end up writing MIME construction code, managing SMTP connections, and debugging encoding issues that have nothing to do with your actual application. Every time I reach for email.mime.multipart, I wonder why I'm doing this in 2026.

An email API skips all of that. This guide covers sending email from Python using the Sendkit email API, from basic text messages through HTML, attachments, and proper error handling.

Setting up the Sendkit Python SDK

Install the package:

pip install sendkit

Grab an API key from your Sendkit dashboard. Keys starting with sk_live_ send real email. Keys starting with sk_test_ go through the same validation but don't actually deliver anything, which is what you want for automated tests.

from sendkit import Sendkit

client = Sendkit("sk_live_your_api_key")

Keep your API key out of source code. Use environment variables or a secrets manager:

from sendkit import Sendkit

# Reads the SENDKIT_API_KEY environment variable automatically
client = Sendkit()

Send a plain text email

The simplest send looks like this:

result = client.emails.send(
    from_="[email protected]",
    to="[email protected]",
    subject="Your order has shipped",
    text="Order #4821 shipped via FedEx. Tracking number: 7489201384.",
)

print(result["id"])  # msg_8f2a...

The from_ address must be on a domain you've verified in Sendkit. (The parameter is from_ with a trailing underscore because from is a reserved keyword in Python.) If you haven't set up domain authentication yet, check out the guide on configuring DMARC, DKIM, and SPF first.

The to field takes a single email string or a list of strings if you're sending to multiple recipients.

Send HTML email

Most emails need some formatting. Pass both text and html so mail clients that can't render HTML still show something readable.

def send_welcome_email(user_email, user_name):
    result = client.emails.send(
        from_="[email protected]",
        to=user_email,
        subject=f"Welcome, {user_name}",
        text=f"Hey {user_name}, thanks for signing up. Your account is ready.",
        html=f"""
        <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
            <h2>Welcome aboard, {user_name}</h2>
            <p>Your account is set up and ready to go.</p>
            <p>Here are a few things to try first:</p>
            <ul>
                <li>Complete your profile</li>
                <li>Connect your first integration</li>
                <li>Invite your team</li>
            </ul>
            <a href="https://app.yoursite.com/getting-started"
               style="background: #0066ff; color: white; padding: 12px 24px;
                      text-decoration: none; border-radius: 4px;
                      display: inline-block; margin-top: 16px;">
                Get started
            </a>
        </div>
        """,
    )
    return result["id"]

Use inline CSS. Email clients strip <style> blocks and ignore external stylesheets, so inline styles are the only approach that works reliably across Gmail, Outlook, Apple Mail, and everything else.

Email notification on a screen

Send email with attachments

Attach files by passing a list of dictionaries. Each one needs a filename, the content as bytes, and a content_type.

def send_invoice(recipient, invoice_number, pdf_path):
    with open(pdf_path, "rb") as f:
        pdf_bytes = f.read()

    result = client.emails.send(
        from_="[email protected]",
        to=recipient,
        subject=f"Invoice #{invoice_number}",
        text=f"Invoice #{invoice_number} is attached.",
        html=f"<p>Invoice #{invoice_number} is attached.</p>",
        attachments=[
            {
                "filename": f"invoice-{invoice_number}.pdf",
                "content": pdf_bytes,
                "content_type": "application/pdf",
            }
        ],
    )
    return result["id"]

Total attachment size should stay under 10MB. For anything larger, upload the file somewhere and include a download link instead. Large attachments slow down delivery and get flagged by spam filters more often.

Send to multiple recipients

You can send to several people in one API call:

result = client.emails.send(
    from_="[email protected]",
    to=["[email protected]", "[email protected]"],
    subject="[ALERT] Database connection pool exhausted",
    text="Connection pool hit 100% at 14:32 UTC. Auto-scaling triggered.",
)

For batch sends where each recipient gets slightly different content, loop and send individually. Or use Sendkit's template system, which handles personalization server-side. The API docs cover template syntax in detail.

Before sending to a list of addresses, it's worth validating them. Sending to invalid addresses hurts your sender reputation. Sendkit has built-in email validation that catches typos, disposable addresses, and dead mailboxes. There's a separate guide on validating email addresses before sending if you want the full picture.

Error handling

APIs fail. Networks drop. Rate limits exist. If you only write the happy path, you'll find out about these at 2am when your password reset flow stops working.

from sendkit import Sendkit, SendkitError

client = Sendkit()  # reads SENDKIT_API_KEY from environment


def send_email_safely(params):
    try:
        result = client.emails.send(**params)
        return {"success": True, "message_id": result["id"]}

    except SendkitError as e:
        if e.status_code == 422:
            # Bad input: invalid address, missing required field
            print(f"Validation error: {e.message}")
            return {"success": False, "error": e.message, "retryable": False}

        if e.status_code == 429:
            # Rate limited
            print(f"Rate limited: {e.message}")
            return {"success": False, "error": "rate_limited", "retryable": True, "retry_after": 60}

        if e.status_code >= 500:
            # Something broke on Sendkit's side
            print(f"Server error: {e.status_code}")
            return {"success": False, "error": "server_error", "retryable": True}

        # Catch-all for other API errors
        print(f"API error ({e.name}): {e.message}")
        return {"success": False, "error": str(e), "retryable": False}

    except Exception as e:
        # Network timeout, DNS failure, etc.
        print(f"Unexpected error: {e}")
        return {"success": False, "error": "network_error", "retryable": True}

For production, add retry logic with exponential backoff:

import time


def send_with_retry(params, max_retries=3):
    for attempt in range(1, max_retries + 1):
        result = send_email_safely(params)

        if result["success"] or not result.get("retryable"):
            return result

        delay = result.get("retry_after", min(2 ** attempt, 30))
        print(f"Attempt {attempt} failed. Retrying in {delay}s...")
        time.sleep(delay)

    return {"success": False, "error": "max_retries_exceeded", "retryable": False}

Three retries handles most transient failures. If things are still broken after that, push the email into a queue (Redis, Celery, whatever you're already using) rather than blocking the user's request.

Server terminal with code running

Using SMTP instead of the API

If your codebase already uses smtplib or a library like django.core.mail, you don't have to rewrite everything. Sendkit provides an SMTP relay you can swap in:

import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg["Subject"] = "Password reset"
msg.set_content("Click here to reset your password: https://app.yoursite.com/reset?token=abc123")

with smtplib.SMTP("smtp.sendkit.com", 587) as server:
    server.starttls()
    server.login("sendkit", "sk_live_your_api_key")
    server.send_message(msg)

Your API key doubles as your SMTP password. No separate credentials to manage.

The API is still the better option for new code. It's faster because there's no SMTP handshake overhead, the response includes a message ID immediately, and you get structured error responses instead of parsing SMTP reply codes. But SMTP is fine when you need to plug into an existing setup.

Django integration

If you're running Django, point the email backend at Sendkit's SMTP:

# settings.py
EMAIL_HOST = "smtp.sendkit.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = "sendkit"
EMAIL_HOST_PASSWORD = "sk_live_your_api_key"
DEFAULT_FROM_EMAIL = "[email protected]"

Then django.core.mail.send_mail works as usual, and all your email goes through Sendkit with full tracking.

After the email leaves your server

Getting the send() call to work is the easy part. Whether the email actually reaches the inbox is a different problem.

First: set up domain authentication. DKIM, SPF, and DMARC are table stakes. Without them, Gmail and Outlook will either spam-folder your messages or reject them entirely. The DMARC/DKIM/SPF setup guide walks through the DNS records.

Second: watch your bounces. Sending repeatedly to addresses that don't exist will tank your sender reputation, and once that's damaged, it affects every email you send, not just the bad ones. Sendkit suppresses bounced addresses automatically, but keep an eye on your bounce rate in your own system too.

Third: validate addresses before you send to them. If you accept user-submitted email addresses, a validation call catches typos like gmial.com and disposable addresses that will never engage. Worth it.

The Sendkit docs cover webhooks, templates, and batch sending if you need to go deeper.

Wrapping up

That's it. Install sendkit, call client.emails.send(), handle errors properly, and you have working email in your Python app.

The part that takes ongoing effort is deliverability. Authenticate your domain, don't ignore bounces, and validate addresses before sending. The sending code is a few lines. Keeping a good sender reputation is the actual work.

Share this article