SMTP vs API for Transactional Email: When to Use Each
A practical decision framework for choosing between SMTP and HTTP API when sending transactional emails — with code samples in Node and Python for both paths.
By JustEmails Platform Team
SMTP vs API for Transactional Email: When to Use Each
Last month I spent three hours debugging why a client's password reset emails weren't sending from their Vercel Edge Function. The culprit? They were trying to use SMTP in a serverless environment that doesn't support long-lived TCP connections. Switched to HTTP API, deployed, done. Sometimes the choice is that obvious. Sometimes it isn't.
If you're wiring up transactional email — password resets, order confirmations, welcome emails — you've got two paths: SMTP relay or HTTP API. Both work. Both deliver email. But they're not interchangeable, and picking the wrong one for your stack means debugging sessions like the one I just described.
Here's the decision framework we use at JustEmails, plus working code for both paths.
What We're Building
By the end of this, you'll have working transactional email sending in your app using both SMTP and HTTP API methods. You'll know which to use when, and you'll have copy-paste code in Node.js and Python for each approach.
Prerequisites
- Node.js 18+ or Python 3.9+
- A transactional email provider account (JustEmails, SendGrid, Postmark, or similar)
- SMTP credentials and/or API key from your provider
- Basic familiarity with async/await patterns
The Decision Framework
Before we write any code — here's when to use each.
Use SMTP when:
- Your app already speaks SMTP (WordPress, older CMS platforms, legacy enterprise apps)
- You need provider portability (SMTP is standardized; switch providers by changing host/credentials)
- Your infrastructure blocks outbound HTTPS but allows port 587 (yes, this exists in some enterprise environments)
- You're integrating with something that only supports SMTP (lots of older SaaS tools fall here)
Use HTTP API when:
- You're running serverless or edge functions (Vercel Edge, Cloudflare Workers, AWS Lambda)
- You need granular event webhooks (opens, clicks, bounces, complaints — SMTP gives you none of this)
- You want structured error responses (HTTP returns JSON; SMTP returns cryptic status codes)
- You're sending high volume and want connection pooling handled for you
- You need template rendering on the provider side
Look, here's the honest take: if you're starting fresh in 2026 with a modern stack, HTTP API is usually the better choice. But "usually" isn't "always," and I've seen enough edge cases to know that SMTP isn't going anywhere.
Step 1: Setting Up SMTP (Node.js)
We'll use Nodemailer — it's been the standard Node SMTP library for a decade and it still works great.
npm install nodemailer
// send-email-smtp.js
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: 'smtp.justemails.app',
port: 587,
secure: false, // STARTTLS on 587, not implicit TLS
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
async function sendPasswordReset(toEmail, resetLink) {
const result = await transporter.sendMail({
from: '"YourApp" <noreply@yourdomain.com>',
to: toEmail,
subject: 'Reset your password',
text: `Click here to reset your password: ${resetLink}`,
html: `<p>Click <a href="${resetLink}">here</a> to reset your password.</p>`
});
console.log('Message sent:', result.messageId);
return result;
}
// Usage
sendPasswordReset('user@example.com', 'https://yourapp.com/reset?token=abc123');
That's it. Nodemailer handles the SMTP handshake (EHLO, AUTH, MAIL FROM, RCPT TO, DATA, QUIT) behind the scenes.
Why this works: Nodemailer maintains a connection pool by default. If you're sending multiple emails, it reuses the TCP connection instead of reconnecting each time. This is important — SMTP connection setup adds 100-300ms of latency.
Step 2: Setting Up SMTP (Python)
Python's built-in smtplib works fine, but I prefer aiosmtplib for async apps.
pip install aiosmtplib
# send_email_smtp.py
import asyncio
import aiosmtplib
from email.message import EmailMessage
import os
async def send_password_reset(to_email: str, reset_link: str):
message = EmailMessage()
message["From"] = "YourApp <noreply@yourdomain.com>"
message["To"] = to_email
message["Subject"] = "Reset your password"
message.set_content(f"Click here to reset your password: {reset_link}")
message.add_alternative(
f"<p>Click <a href='{reset_link}'>here</a> to reset your password.</p>",
subtype="html"
)
await aiosmtplib.send(
message,
hostname="smtp.justemails.app",
port=587,
start_tls=True,
username=os.environ["SMTP_USER"],
password=os.environ["SMTP_PASS"]
)
print(f"Email sent to {to_email}")
# Usage
asyncio.run(send_password_reset("user@example.com", "https://yourapp.com/reset?token=abc123"))
Same idea, different language. The async version matters if you're in a FastAPI or async Django context — blocking SMTP calls will tank your throughput.
Step 3: Setting Up HTTP API (Node.js)
No library needed. Just fetch (built into Node 18+).
// send-email-api.js
async function sendPasswordReset(toEmail, resetLink) {
const response = await fetch('https://api.justemails.app/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.EMAIL_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
from: { email: 'noreply@yourdomain.com', name: 'YourApp' },
to: [{ email: toEmail }],
subject: 'Reset your password',
text: `Click here to reset your password: ${resetLink}`,
html: `<p>Click <a href="${resetLink}">here</a> to reset your password.</p>`
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Email failed: ${error.message}`);
}
const result = await response.json();
console.log('Message ID:', result.id);
return result;
}
The beauty here? Works everywhere. Vercel Edge Functions, Cloudflare Workers, Deno Deploy, Bun — anywhere that can make HTTPS requests.
(Substitute your provider's API endpoint. SendGrid is api.sendgrid.com/v3/mail/send, Postmark is api.postmarkapp.com/email, etc. The payload structure varies, but the pattern is the same.)
Step 4: Setting Up HTTP API (Python)
# send_email_api.py
import httpx
import os
async def send_password_reset(to_email: str, reset_link: str):
async with httpx.AsyncClient() as client:
response = await client.post(
"https://api.justemails.app/v1/send",
headers={
"Authorization": f"Bearer {os.environ['EMAIL_API_KEY']}",
"Content-Type": "application/json"
},
json={
"from": {"email": "noreply@yourdomain.com", "name": "YourApp"},
"to": [{"email": to_email}],
"subject": "Reset your password",
"text": f"Click here to reset your password: {reset_link}",
"html": f"<p>Click <a href='{reset_link}'>here</a> to reset your password.</p>"
}
)
if response.status_code != 200:
raise Exception(f"Email failed: {response.json()}")
result = response.json()
print(f"Message ID: {result['id']}")
return result
I use httpx instead of requests because it supports async natively. If you're in a sync context, requests works too — swap async with httpx.AsyncClient() for requests.post() and drop the await.
Common Errors and Fixes
These are the ones I hit most often. Saving you the debugging time.
SMTP: Connection timed out on port 587
Error: connect ETIMEDOUT smtp.provider.com:587
Your cloud provider might block outbound SMTP. AWS EC2 blocks port 25 by default (you can request removal), and some VPCs restrict 587 too. Cloudflare Workers and Vercel Edge don't support SMTP at all — that's a TCP protocol thing, not a firewall issue.
Fix: Switch to HTTP API, or if you're on EC2, request SMTP throttle removal via AWS support.
SMTP: 535 Authentication failed
Error: 535 5.7.8 Error: authentication failed
Wrong credentials, or your provider requires app-specific passwords. If you're using Gmail SMTP (don't, for transactional email, but people do), you need an app password, not your account password.
Fix: Regenerate your SMTP credentials in your provider's dashboard. Copy-paste carefully — trailing whitespace breaks things.
API: 401 Unauthorized
{"error": "Invalid API key"}
Usually a Bearer prefix issue. Some providers want Authorization: Bearer sk_xxx, others want Authorization: api-key sk_xxx, others want X-Api-Key: sk_xxx.
Fix: Check your provider's docs for the exact header format. It's not standardized.
API: 429 Too Many Requests
{"error": "Rate limit exceeded", "retry_after": 60}
You're hitting your provider's rate limit. SendGrid's free tier is 100 emails/day. Postmark is 100/month on trial. JustEmails includes 1,000 transactional emails/month on the base $49/year plan, with stackable +$25/year per additional 10K/month tier.
Fix: Implement exponential backoff, or batch your sends. Or upgrade your plan — transactional email at volume isn't free.
When the Answer Is "Both"
Real talk — some apps need both. We run SMTP for our internal ticket system (it only supports SMTP) and HTTP API for customer-facing transactional emails (we need delivery webhooks). Same JustEmails account, same domain, two different integration methods.
Most providers let you do this. SendGrid, Postmark, Mailgun, JustEmails — they all support SMTP and API simultaneously. You're not locked into one.
If you're building something new, start with HTTP API. You can always add SMTP later if some legacy system needs it. The reverse is harder — migrating from SMTP to API means touching every integration point.
Next Steps
Now that you've got email sending working, you probably want to:
- Set up webhooks to track bounces and complaints. We covered webhook setup in our email deliverability guide — same DNS patterns apply.
- Add retry logic for transient failures. Both SMTP and API can fail temporarily; don't drop emails on the floor.
- Monitor your sender reputation with Google Postmaster Tools. High bounce rates or spam complaints will tank your deliverability regardless of which method you use.
If you're evaluating transactional email providers, we put together a cost comparison in our Google Workspace alternatives post — the math applies to transactional email too.
Questions? We're at support@justemails.app. And if you're using JustEmails for transactional email (or thinking about it), the API docs are at docs.justemails.app. We've also got client libraries in Node, Python, Ruby, Go, and PHP — but honestly, raw HTTP is fine for most use cases. The libraries just handle retry logic.
Frequently Asked Questions
Should I use SMTP or API for transactional emails?
Use SMTP when your app already speaks SMTP (legacy systems, WordPress, existing CMS platforms), when you need portability between providers, or when you're running on infrastructure that blocks outbound HTTP but allows port 587. Use HTTP API when you're in serverless/edge environments, need granular webhook events (opens, clicks, bounces), want better error handling, or need to send high volumes with connection pooling.
Is SMTP slower than HTTP API for sending email?
Not meaningfully for single emails — both complete in under 500ms typically. But SMTP has connection overhead (EHLO, AUTH, MAIL FROM, RCPT TO, DATA, QUIT) that adds latency when sending many emails without connection reuse. HTTP APIs batch better and handle connection pooling on their end. For 10K+ emails/hour, API usually wins on throughput.
Can I switch from SMTP to API later without changing providers?
Yes, most transactional email providers (JustEmails, SendGrid, Postmark, Mailgun) support both SMTP and HTTP API with the same account and credentials. You're not locked in. Start with whichever fits your current stack, and switch when your needs change.
What port should I use for SMTP transactional email?
Port 587 with STARTTLS is the standard for authenticated SMTP submission. Port 465 (implicit TLS) works too but is less universally supported. Avoid port 25 — it's meant for server-to-server relay, not client submission, and many cloud providers block it outright.
Try JustEmails
Unlimited custom domain email hosting for $49/year flat — unlimited domains, unlimited mailboxes, 10 GB storage, full IMAP/SMTP. Built for agencies, freelancers, and anyone managing email across more than one domain.