Technical Guide

SMTP vs Email API: Which Should You Use?

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.

9 min read·Updated March 2026

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.

1. How SMTP works

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.

# What SMTP actually looks like over the wire
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.

2. How an email API works

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.

# Sending via HTTP API — the whole thing is one request
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.

3. Side-by-side comparison

DimensionSMTPEmail API
ProtocolTCP, stateful connectionHTTPS, stateless REST
AuthenticationUsername + password (SMTP AUTH)Bearer token in header
Error handlingNumeric SMTP codes (550, 421, 452…) that vary by serverStandard HTTP status codes + structured JSON error body
Retry logicUsually in library; inconsistent across langsStandard HTTP retry with exponential backoff
Connection poolingRequired for performance; complex to configureHTTP/2 multiplexing handles this automatically
Firewall / egressPort 587/465 often blocked by cloud providers or corp firewallsPort 443 (HTTPS) — always open
Webhooks / eventsNot built in — need separate integrationNative: delivered, bounced, complained, clicked
TemplatesBuild in your app codeStored and versioned server-side
Rate limitingSMTP 421/452 codes — hard to handle gracefullyHTTP 429 with Retry-After header
Ops overheadHigh — manage connection pools, TLS, auth rotationLow — one API key, standard HTTP client

4. Code comparison: nodemailer vs email API

Let's send a password-reset email both ways. Same result, very different code.

SMTP with Nodemailer

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

Email API with fetch (no dependencies)

// 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.

Using server-side templates (API only)

// 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',
    },
  }),
})

5. When to use SMTP

SMTP is the right choice in a smaller number of situations than most developers assume, but those situations do exist:

You're working with a legacy system that only speaks SMTP

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 (and PHP apps using wp_mail)

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.

You want to self-host everything and use Postfix / Exim

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.

Local development / testing with Mailpit or MailHog

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.

6. When to use an email API

For new applications — anything built in the last 5 years — the email API is almost always the better choice:

Modern apps and microservices
REST APIs are a natural fit for applications already structured around HTTP. Your email sending code looks like every other service call.
Serverless and edge functions
Lambda, Vercel Edge, Cloudflare Workers — these environments are stateless and often don't support persistent TCP connections. HTTP APIs work; connection-pooled SMTP doesn't.
You need delivery visibility
Webhooks for bounce, complaint, and open events are only available through APIs. With raw SMTP, you have no feedback loop — you're sending blind.
High volume transactional email
APIs handle rate limiting gracefully with Retry-After headers. SMTP rate-limit responses (421) are less standardized and harder to handle reliably at scale.
You want sequences, templates, or automation
Onboarding drips, re-engagement campaigns, and behavior-triggered emails require logic beyond what raw SMTP provides. APIs give you the primitives to build these — or platforms that handle it natively.
Cloud environments with blocked outbound ports
AWS, GCP, and Azure block port 25 by default. Port 587 works, but many corporate firewalls block it too. Port 443 (HTTPS) is always open.

7. How tinysend bridges both worlds

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:

Delivery via your own SES

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.

REST API + sequences

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.

Contact management

Contacts, segments, suppression lists, and unsubscribe handling — all managed by tinysend. Your app just fires events.

Webhook events

Delivered, bounced, complained, opened, clicked — all forwarded to your endpoint in real time so you can react programmatically.

# Trigger an onboarding sequence via tinysend API
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.

Switch from SMTP to a real email API

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 →