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.
This is not theoretical pain. Chong on our team lost time to it while wiring up an internal app: mail silently refused to move until he found the SMTP port misconfiguration and fixed it. And the failure modes get worse once reputation enters. Sends from a brand-new sending domain with no history land in spam or get greylisted at Gmail until warmup settles it.
The setup lesson underneath: Gmail and the other inbox providers expect senders on a subdomain. Our sending-domains guide says it plainly, send from a subdomain you own for the best inbox placement. It also keeps your DNS out of trouble. Sending records on send.yourdomain.com also stay clear of the records a mailbox provider such as Google Workspace adds at the apex. The basics are collected in our How To Send An Email guide in the email concepts section.
One POST, and the send is handled
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 a 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. A seed-address test still belongs in the preflight, whatever the validator says.
That flag exists because we designed the send path against accidents. George, our CEO, wrote the campaign-send-safety spec that shaped it: reject unknown parameters, require an explicit confirm before a real send, offer dry_run so an agent can check the recipient count first, and give transactional mail its own proper endpoint, POST /templates/{id}/send with real unsubscribe links, instead of a repurposed test-send. He also holds renderer merges to a client-validation bar. A real Outlook pass, a real Gmail app pass, a real Yahoo pass, because local QA cannot exercise Outlook.
Signup confirmations and password resets are exactly what that endpoint carries in production. One pattern that works: render every confirm and reset link inside your own app and hand the finished message to the transactional API, no merge tags involved. The API does not force a template layer on mail your code already knows how to build.
Full parameters, responses, and error codes live in the REST API docs and the API reference.
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.
We are not inventing a new communication protocol here. It is all email, controlled by the agent, and whether an agent or a human replies is outside the scope of the implementation. That is the point.
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. If you are staying on SMTP, how to set up SMTP is the setup to follow, and SPF, DKIM and DMARC authentication is what stops those sends landing in spam.
requests versus an SDK is the kind of decision a Python developer makes once and forgets about. Increasingly nobody has to make it. Point Claude, Cursor, or Codex at our MCP server and the agent composes the send itself, with you approving before anything goes out, script or no script. That's the shift I built Nitrosend for, and it's why we're a listed app in the ChatGPT app marketplace rather than just another pip-installable client.
Go deeper
FAQ
How do I send an email with Python?
Make one authenticated HTTP POST to an email API with the requests library: from, to, subject, and html in a JSON body, a bearer token in the header. You get a message ID back and delivery events on your webhook. No smtplib, no SMTP server, no app passwords.
Should I use smtplib or an email API?
Use smtplib for a personal script on your own machine. Use an email API for anything a product depends on: it removes SMTP connection management, survives provider auth changes, and adds deliverability, tracking, and delivery events.
Can Python send email through Gmail?
Yes, with an app password over SMTP, but app passwords expire, security alerts fire, and the daily cap is built for a person. For production email, send through a platform and keep Gmail as your mailbox.