Send email with Python

Sending email with Python is one authenticated HTTP request. No smtplib, no ports, no app passwords, no MIME assembly. POST the message, get a message ID back.

Verified By George Hartley, Co-founder · Updated July 20, 2026

The smtplib way, and what it costs you

Every Python email tutorial starts the same way: import smtplib, connect to smtp.gmail.com, pick port 587 or 465, generate an app password, build a MIME message. It works, on your machine, today.

Then the app password expires. Or port 25 is blocked on the server. Or Gmail's daily cap hits, because that cap was built for a person, not a product. smtplib sends mail; it does nothing for deliverability, tracking, or what happens after the send.

One POST, and the send is handled

PythonPOST /v1/my/messages
import requests

requests.post(
  "https://api.nitrosend.com/v1/my/messages",
  headers={"Authorization": "Bearer $NITROSEND_API_KEY"},
  json={
    "from": "[email protected]",
    "to": "[email protected]",
    "subject": "Welcome",
    "html": "<p>You're in.</p>",
  },
)

That is the whole integration. The requests library you already have, a bearer token from the environment, and a JSON body. SPF, DKIM, and DMARC are handled on the platform side, which is the part that decides whether your email lands in the inbox.

And before anything ships, run my preflight. Resolve every wrapped CTA URL, check the spam score, weigh images against text, lint for dark mode, and send a test to a seed address. Some platforms build this in. If yours doesn't, do it yourself, every time.

The API also takes a dry_run flag: pass it and the call validates recipients and configuration without sending anything, which is exactly what you want in a test suite. Treat it as a check on the request, not a guarantee about delivery. I've watched a dry run validate cleanly and the real send still fail on something the environment only reveals live, which is why the seed-address test stays in my preflight no matter what the validator says.

Full parameters, responses, and error codes live in the REST API docs and the API reference.

Python Node.js PHP Go Rust +5
send.py requests ยท stdlib-adjacent
import requests

requests.post(
  "https://api.nitrosend.com/v1/messages",
  headers={"Authorization": f"Bearer {KEY}"},
  json={"to": to, "subject": subj,
        "html": body})
202 ACCEPTED
{ "id": "msg_9f2c",
  "status": "accepted" }

And the part no library gives you

The same account exposes an MCP server, so the agent writing your code can also run your email. Point Claude, Cursor, or Codex at api.nitrosend.com/mcp and it composes, previews, and stages sends, with a human approving every one. The API is for your code. The MCP is for your agent. One platform either way.

Every send comes back with a message ID, and delivery events follow on your webhook: delivered, opened, bounced. No dashboard required to know what happened.

Go deeper

First send in thirty seconds.

Simple pricing. Unlimited contacts.

Every plan includes full stack emailing: Flows, Newsletter Campaigns and Transactional Email, plus our NitroWheel LLM and all agent integrations (Claude, ChatGPT, Codex, Cursor and others). Pay for what you send, not who you store.

Free
$0
forever
  • Emails 8,000then 500/mo
  • Email types Transactional & Marketing
  • AI actions 20/mo
  • Contacts Free & Unlimited
  • Brands 1
  • Seats 1
Start free
Ultra
$100
per month
  • Emails 125,000/month
  • AI actions 5,000/mo
  • Brands 5
  • Seats 10 · Domains 3
  • Frontier AI Included
  • Dedicated IP Available
Get started
Enterprise
$300
per month
  • AI actions Unlimited
  • Unlimited brands & domains Included
  • SSO / SAML Included
  • 99.9% SLA Included
Get started

Free forever. No credit card required. See full comparison →