Email API vs SMTP: Which Should Your Application Use?
Use an email API for most new web applications when you want a structured request, an immediate machine-readable response, and provider-specific features. Use SMTP when you need protocol portability, an existing mail-library integration, or a relay you already operate.
That is the short answer. The important part is what it does not decide. API versus SMTP does not change the recipient domain's MX records, make SPF or DKIM pass, prove that a mailbox exists, or guarantee delivery. It chooses how your application hands a message to its sending service.
I make this decision by tracing one message across the handoff boundary, especially the failure cases. The happy-path syntax is the least interesting difference.
The real choice is your submission boundary
SMTP is a standardized mail-submission protocol. RFC 6409 reserves port 587 for message submission, and RFC 8314 defines implicit TLS submission on port 465. Your application or local relay opens an SMTP session, authenticates, sends the envelope, transfers the message, and waits for the server's reply.
An email API is usually a provider-defined HTTP endpoint. You may send structured fields, template variables, or a raw message, depending on that API. There is no single internet standard that makes every email API behave the same way, so its idempotency, validation, attachment limits, and event model come from the provider's contract.
| Decision point | Email API | SMTP submission |
|---|---|---|
| Request shape | Provider-defined HTTP request | Standard SMTP commands plus an email message |
| Port and TLS | Normally HTTPS | Usually 587 with STARTTLS or 465 with implicit TLS |
| Templates and metadata | Often first-class, but provider-specific | Usually composed by your application or relay |
| Idempotency | Possible when the API explicitly supports it | No standard submission idempotency key |
| Delivery events | Usually a separate provider webhook or event API | DSNs, a relay log, or provider events |
| Portability | Requires an adapter for each API | Broad protocol support, with provider-specific authentication and limits |
The table describes common interfaces, not promises. An API can accept raw MIME. An SMTP provider can expose excellent webhooks. A provider can support both paths and feed them into the same downstream delivery system.
Compare failure ownership before syntax
With either transport, an accepted submission is not a delivered message. It means the next system has taken responsibility for more work.
In SMTP, the decisive moment is the reply after the message data. RFC 5321 says a server that returns success after the end of the message accepts responsibility for delivery or for reporting a later failure. Your application should not keep trying to deliver directly to the recipient's MX after a submission server accepts it. That queue now belongs to the submission service.
C: MAIL FROM:<alerts@app.example>
S: 250 2.1.0 Sender accepted
C: RCPT TO:<person@recipient.example>
S: 250 2.1.5 Recipient accepted
C: DATA
S: 354 Send message content
C: ...message bytes...
C: .
S: 250 2.0.0 Queued as 8F42A
This is a hypothetical trace. The final 250 means the submission server queued the message. It does not mean the recipient server accepted it, that it reached an inbox, or that a human owns the address.
An HTTP API makes the handoff look different, but the responsibility boundary is similar:
POST /messages HTTP/1.1
Authorization: Bearer ...
Idempotency-Key: delivery_01J...
Content-Type: application/json
{"to":"person@recipient.example","template":"receipt","data":{"order":"A123"}}
HTTP/1.1 202 Accepted
Content-Type: application/json
{"message_id":"msg_7d9...","status":"queued"}
This example is also hypothetical. The endpoint, header, and response fields are not universal. Treat them as real only when your provider documents them.
Do not let a timeout create two emails
The hardest production case is not a clean rejection. It is an ambiguous handoff.
Suppose an API accepts a POST, queues the message, and then the connection closes before your application reads the response. Retrying may submit a duplicate. RFC 9110 does not define POST as idempotent, so a generic HTTP client cannot assume the retry is safe. A provider-specific idempotency key can close that gap, but only if the provider guarantees its scope and retention window.
SMTP has its own ambiguous window. The server may have accepted the message after the terminating dot while the connection failed before the client received the final reply. SMTP does not define a submission idempotency key. Reusing the same Message-ID can help your own correlation, but it does not require the receiving submission server to deduplicate the message.
The fix belongs above both transports: create one durable outbound delivery record before attempting submission.
{
"delivery_id": "delivery_01J...",
"recipient": "person@recipient.example",
"template": "receipt_v3",
"state": "queued",
"attempt": 0,
"provider_message_id": null
}
A worker claims that record, submits it once, and stores the provider's response. If the result is ambiguous, move the record to an explicit unknown_submission state. Reconcile through a provider lookup, event stream, or controlled retry policy instead of pretending the first attempt definitely failed.
The duplicate transactional email guide covers the outbox and idempotency pattern in more detail. The transport choice does not remove the need for it.
Keep the application contract transport-neutral
I would not spread SMTP commands or one provider's JSON fields through signup, billing, and notification code. Put one small contract between product logic and message submission:
type OutboundEmail = {
deliveryId: string
from: string
to: string[]
template: string
data: Record<string, unknown>
replyTo?: string
}
type SubmissionResult =
| { state: 'accepted'; providerId: string }
| { state: 'rejected'; code: string; retryable: boolean }
| { state: 'unknown'; reason: string }
An API adapter can translate the contract into JSON. An SMTP adapter can render the template, construct the MIME message, and submit it to a relay. The application still sees the three states that matter: accepted, rejected, or unknown.
Keep delivery events separate from submission responses. A later delivered, bounced, delayed, or complained event should reference the stored delivery record. Authenticate webhook requests, reject replays, and make event processing idempotent. The email webhook security walkthrough owns that boundary.
Choose by workload, not fashion
For a new SaaS application, I would usually start with an email API. HTTPS fits common application infrastructure, structured errors are easier to normalize, and provider-specific templates or idempotency can be useful. That is a default, not a law.
SMTP is often the better fit when you already have a trusted relay, when a mature application only knows how to hand off mail through SMTP, or when you want the submission layer to stay behind a standard protocol. It can also be the cleanest boundary for many internal applications that should never hold an external email API credential.
Use this decision order:
- Start with the operational owner. Decide whether your application or an existing relay should construct, authenticate, queue, and observe submissions.
- Map the ambiguous failure. Document what happens when the sender accepts a message but your client loses the response.
- Check the event path. Submission without authenticated bounce and complaint handling is only half an integration.
- Match the network. Confirm that your environment permits the required HTTPS or SMTP egress and can validate TLS certificates.
- Test provider limits. Message size, recipient count, rate limits, templates, and raw-message support are contract details, not protocol folklore.
- Keep an exit seam. Store your own delivery ID and normalized states so a transport change does not rewrite product logic.
The strongest objection is that SMTP is more portable, so choosing it avoids provider lock-in. It reduces one kind of lock-in, but it does not erase provider differences. Authentication, throttling, bounce events, suppression behavior, limits, and observability still vary. Portability comes from your adapter and stored delivery contract, not from the socket alone.
Authentication and verification stay separate
Whether you submit over HTTP or SMTP, the outbound domain still needs correct sender authentication. SPF, DKIM, and DMARC are evaluated later in the mail path. The SPF, DKIM, and DMARC guide explains why their identities must align.
Recipient verification is a different decision again. A verification API can evaluate syntax, mail routing, risk signals, and available SMTP evidence before a send. It may also return unknown when the evidence is inconclusive. It does not prove inbox ownership, and neither a 202 API response nor a final SMTP 250 guarantees delivery.
If you are adding that pre-send check, the production result-handling guide keeps deliverable, risky, undeliverable, and unknown states explicit. The current EmailVerifier API reference documents the request and response fields.
My rule is simple: choose API or SMTP by the handoff you can operate safely, then design retries and delivery evidence as if the network will fail at the worst possible byte. Eventually, it will.