Technical Guide
SMTP has been the backbone of email for 40 years. Modern email APIs wrap it in a JSON interface and fix its worst problems. Here's when to use each — with real code so you can decide.
If you're building an app that sends email — password resets, receipts, notifications — you'll face this choice: SMTP or an email API? They both deliver mail, but the developer experience, reliability, error handling, and operational overhead are very different.
SMTP (Simple Mail Transfer Protocol) was designed in 1982 and hasn't changed fundamentally since. Your app opens a TCP connection to port 587 (or 465 for TLS), authenticates with a username and password, then sends the raw email message in a line-by-line text protocol.
S: 220 smtp.example.com ESMTP Postfix C: EHLO myapp.example.com S: 250-smtp.example.com S: 250-AUTH LOGIN PLAIN S: 250 STARTTLS C: STARTTLS S: 220 Go ahead C: AUTH PLAIN [base64 credentials] S: 235 Authentication successful C: MAIL FROM:<[email protected]> S: 250 Ok C: RCPT TO:<[email protected]> S: 250 Ok C: DATA S: 354 End data with <CR><LF>.<CR><LF> C: From: [email protected] C: To: [email protected] C: Subject: Welcome! C: C: Hello, welcome to the app. C: . S: 250 Ok: queued as 12345 C: QUIT
Libraries like Nodemailer (Node.js), smtplib (Python), and ActionMailer (Rails) abstract this into a friendlier API — but they're still establishing TCP connections, managing TLS handshakes, and speaking SMTP underneath. That complexity leaks into your application.
The SMTP relay model: Most apps don't run their own SMTP server — they use a relay like Gmail SMTP, Amazon SES SMTP endpoint, or SendGrid's SMTP gateway. Your app speaks SMTP to the relay; the relay handles actual delivery. This is important: the protocol is SMTP, but the infrastructure is someone else's.
An email API is an HTTPS endpoint that accepts JSON (or form data). Under the hood, it converts your JSON payload to the SMTP format and delivers it — but you never touch the wire protocol. You POST a structured object and get back a structured response.
POST https://api.tinysend.co/v1/emails
Authorization: Bearer ts_live_your_api_key
Content-Type: application/json
{
"from": "[email protected]",
"to": "[email protected]",
"subject": "Welcome!",
"html": "<h1>Welcome</h1><p>Hello, welcome to the app.</p>",
"text": "Welcome to the app."
}
// Response (200 OK)
{
"id": "email_01HXYZ...",
"status": "queued"
}No TCP connection management. No SMTP session state. No base64 encoding credentials. No DATA command. Just JSON in, JSON out — same as any other REST API your app talks to.
Modern email APIs also expose webhooks for delivery events (delivered, bounced, complained, opened, clicked), making it easy to react to what happens after you send.
| Dimension | SMTP | Email API |
|---|---|---|
| Protocol | TCP, stateful connection | HTTPS, stateless REST |
| Authentication | Username + password (SMTP AUTH) | Bearer token in header |
| Error handling | Numeric SMTP codes (550, 421, 452…) that vary by server | Standard HTTP status codes + structured JSON error body |
| Retry logic | Usually in library; inconsistent across langs | Standard HTTP retry with exponential backoff |
| Connection pooling | Required for performance; complex to configure | HTTP/2 multiplexing handles this automatically |
| Firewall / egress | Port 587/465 often blocked by cloud providers or corp firewalls | Port 443 (HTTPS) — always open |
| Webhooks / events | Not built in — need separate integration | Native: delivered, bounced, complained, clicked |
| Templates | Build in your app code | Stored and versioned server-side |
| Rate limiting | SMTP 421/452 codes — hard to handle gracefully | HTTP 429 with Retry-After header |
| Ops overhead | High — manage connection pools, TLS, auth rotation | Low — one API key, standard HTTP client |
Let's send a password-reset email both ways. Same result, very different code.
import nodemailer from 'nodemailer'
// Create a transport — this manages the SMTP connection pool
const transport = nodemailer.createTransport({
host: 'smtp.mailprovider.com',
port: 587,
secure: false, // true for 465, false for 587
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
pool: true, // keep connections open
maxConnections: 5,
maxMessages: 100,
})
// Send a password reset email
async function sendPasswordReset(userEmail, resetToken) {
const resetUrl = `https://myapp.com/reset?token=${resetToken}`
const result = await transport.sendMail({
from: '"My App" <[email protected]>',
to: userEmail,
subject: 'Reset your password',
text: `Click here to reset your password: ${resetUrl}`,
html: `<p>Click <a href="${resetUrl}">here</a> to reset your password.</p>`,
})
// result.messageId is a local ID — no guarantee of delivery
console.log('Queued:', result.messageId)
return result
}
// Error handling — SMTP codes you need to know
// 421: Service temporarily unavailable — retry later
// 450: Mailbox temporarily unavailable
// 550: Mailbox unavailable (hard bounce) — remove from list
// 552: Message too large// No library needed — just native fetch (Node 18+)
async function sendPasswordReset(userEmail, resetToken) {
const resetUrl = `https://myapp.com/reset?token=${resetToken}`
const response = await fetch('https://api.tinysend.co/v1/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TINYSEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: '[email protected]',
to: userEmail,
subject: 'Reset your password',
text: `Click here to reset your password: ${resetUrl}`,
html: `<p>Click <a href="${resetUrl}">here</a> to reset your password.</p>`,
}),
})
if (!response.ok) {
const error = await response.json()
// HTTP 400: invalid payload (your bug)
// HTTP 422: unprocessable — bad email address etc.
// HTTP 429: rate limited — check Retry-After header
// HTTP 5xx: service issue — safe to retry
throw new Error(`Email send failed: ${error.message}`)
}
const { id, status } = await response.json()
console.log(`Email ${id} — status: ${status}`) // "queued" or "sent"
return id
}The API version has no dependency, no connection pool to configure, no SMTP codes to map, no TLS setup, and returns a server-assigned email ID you can use to correlate webhook events later.
// With templates, your app code is tiny — no HTML in JS
const response = await fetch('https://api.tinysend.co/v1/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TINYSEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: '[email protected]',
to: userEmail,
template: 'password-reset',
variables: {
firstName: user.name,
resetUrl: `https://myapp.com/reset?token=${token}`,
expiresIn: '24 hours',
},
}),
})SMTP is the right choice in a smaller number of situations than most developers assume, but those situations do exist:
Some ERPs, CMSes, and legacy enterprise software have SMTP baked in with no alternative. If you can't change the sending code, you need an SMTP relay.
WordPress's wp_mail function uses PHP's built-in mail or a configured SMTP server. Plugins like WP Mail SMTP configure the SMTP relay. You can't really use an HTTP API here without custom code.
Running your own MTA gives you full control but requires managing IP reputation, TLS certificates, blocklist monitoring, and bounce handling. Only worth it at large scale with dedicated ops resources.
During development, tools like Mailpit catch outbound email via SMTP and display it in a local UI. This works because your app already uses SMTP (via Nodemailer) — you just point it at localhost instead of production.
For new applications — anything built in the last 5 years — the email API is almost always the better choice:
Most email APIs are just delivery infrastructure — you send JSON, they deliver it. What they don't handle: sequences, contact management, or letting you use your own Amazon SES keys so you're not paying platform markup on every send.
tinysend is built API-first with a BYOK (Bring Your Own Key) model:
Connect your Amazon SES account. tinysend routes sends through it — you pay SES rates ($0.10/1K) instead of platform markup. Full deliverability, your reputation.
Single API for one-off sends and multi-step sequences. Define sequences in code (YAML/JSON), deploy via CLI. No drag-and-drop builder required.
Contacts, segments, suppression lists, and unsubscribe handling — all managed by tinysend. Your app just fires events.
Delivered, bounced, complained, opened, clicked — all forwarded to your endpoint in real time so you can react programmatically.
await fetch('https://api.tinysend.co/v1/sequences/onboarding/enroll', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TINYSEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
contact: {
email: user.email,
firstName: user.name,
plan: user.plan,
},
// Sequence handles: welcome → day 2 tip → day 7 check-in
// Conditions evaluated server-side using contact properties
}),
})If you're currently on SMTP and want to move: the migration is a one-line change in your send function — swap the transport for an HTTP call. The rest of your application code stays the same.
tinysend gives you transactional sends, sequences, webhooks, and BYOK — so you're not paying platform markup. API-first, usage-based, free to start.
Get started free →