The loop, and where it stops being fine
For a handful of personalized messages, the honest answer is a loop: one POST per recipient, each with its own message ID and its own delivery events. Clean, trackable, done. Every POST authenticates with a server-side API key read from the environment, which is the only place a key with account access should live.
for user in users: requests.post( "https://api.nitrosend.com/v1/my/messages", headers={"Authorization": "Bearer $NITROSEND_API_KEY"}, json={"from": "[email protected]", "to": user["email"], "subject": f"Welcome, {user['name']}", "html": user["body"]}, )
Bulk on the transactional path is a trap
Here is the line I hold: a bulk send should not be built on the transactional messages path. That is a deliverability and compliance trap. Transactional infrastructure is tuned and trusted for one-to-one, event-triggered mail; push a thousand marketing messages through it and you burn the reputation your receipts depend on, with no unsubscribe handling to keep you legal.
Real lists go through campaigns: import the contacts, define the segment, send once, and let the platform handle suppression, unsubscribes, and per-recipient events.
Under the hood, a campaign resolves its recipients into a durable outbox in 5,000-row batches before anything fires, so the send phase reads from a prepared queue instead of segmenting mid-flight. Rate limits bracket the same design: every account sends inside a rolling 24-hour allowance set by its plan and sender standing, 100, 500 or 5,000 recipients on Free, and pacing, circuit and provider limits shape delivery inside it. The platform is built so the wire is the bottleneck, never the queue.
I hold this line because I've watched it get tested. A spammer once bought a paid plan specifically to shove bulk mail through our transactional path, because that path skips CAN-SPAM footers and List-Unsubscribe. That is exactly the loophole your own bulk loop exploits, against your own domain.
Incidents like that are why the infrastructure fails closed at every layer. On our forwarding relay, TCP 2525 is firewall-restricted to our API host, TLS and exact SMTP authentication are mandatory, every single-recipient route and MX snapshot is HMAC-signed, and unsigned or replayed requests are rejected. The loop in your script rides on that enforcement whether you think about it or not.
Full parameters and responses live in the REST API docs and the API reference.
Go deeper
The loop above works, but it is not the deeper point. Describe the list and the message to Claude, Cursor, or Codex instead, and our MCP server resolves it into the same campaign path I described above, with the same suppression and unsubscribe handling, no script to schedule or babysit.
Cleaning the list first matters more than the loop: read the email validation API guide, because every bad address in that loop is a bounce against your domain. Volume is where sloppiness shows up, so track bounce rate and follow transactional email best practices before you scale the list.
FAQ
How do I send email to multiple recipients in Python?
For a few personalized messages, loop one API call per recipient. For a list, use a campaign send: bulk email through the transactional path damages deliverability and skips unsubscribe compliance.
Why not put all recipients in one to field?
Every recipient sees every address, personalization dies, and one bounce taints the shared send. One message per recipient, or a proper campaign.