DEV Community

MaximilianNilsson7568
MaximilianNilsson7568

Posted on

Email API Services for SaaS Onboarding Reliability at 10,000 Sends (and Retention)

Short answer: For SaaS onboarding emails, choose a delivery service with clear acceptance, authentic event evidence, and bounded retry semantics; keep signup state and retention policy in your own system.

The bill for a signup verification flow is rarely dominated by the send itself. At 10,000 signup attempts, the consequential cost is the messages, events, suppression decisions, and delivery evidence you retain long enough to explain why a user never received a link. Design around delivery reliability first: keep the verification state in your system of record, treat the email API as a delivery boundary, and retain only the evidence needed to retry safely and investigate disputes.

The retry boundary is where this design usually breaks.

That changes the selection question. The useful question is not which email API has the longest feature list; it is which delivery service gives your team enough signal to classify temporary failure, permanent rejection, throttling, and user error without making the signup path depend on a provider-specific state model. The correction is operational rather than cosmetic: a service can accept a request while the recipient remains unreachable, so the application needs a state model that preserves that uncertainty instead of converting it into a confident success or failure.

What does one verification email actually cost to retain?

The send request is a small row in a much larger chain. A practical record has an account or signup identifier, a one-time token reference, an expiry timestamp, an idempotency key, the selected recipient, the accepted message identifier, delivery events, and a reason code for the final state. The message body does not belong in every operational log. It is usually the least useful and most sensitive part to keep.

At 10,000 attempts, retaining a compact event record is manageable; retaining every rendered body, provider response, webhook payload, and debug trace indefinitely is a different storage decision. The dominant term becomes retention multiplied by event volume, not the nominal request fee. I would keep the state transition and a bounded audit trail, then expire raw payloads on a documented schedule. The trade-off is uncomfortable but clear: after expiry, a support investigation may prove that an event was accepted without being able to reconstruct every header and template byte.

Record Keep for the signup workflow Why it matters
Verification state Until token expiry, plus a short audit window Prevents duplicate acceptance and supports safe resend
Message identifier and timestamps Through the incident or support window Correlates your request with delivery events
Event type and normalized reason Longer than raw payloads Supports reliability reporting without storing excess content
Rendered body and full webhook payload Short, access-controlled retention Useful for investigation, but sensitive and expensive to keep

Retention is part of reliability. If the system cannot tell “accepted by the delivery boundary” from “delivered to the recipient,” it cannot give support a truthful answer.

Which failure can the signup path safely retry?

A verification link is a small message with a large blast radius. A transient connection failure after the request leaves your process can produce an ambiguous outcome: the email may have been accepted, or it may not have arrived at all. Retrying blindly can send two links, and treating every error as final can strand a legitimate signup.

The application should therefore create its own delivery intent before making the outbound call. Give that intent a stable key derived from the signup attempt, not from a random retry. Store the token hash and expiry locally. On a retry, reuse the intent while applying a policy for temporary errors; do not mint a new account state merely because a network call timed out.

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone


@dataclass
class DeliveryIntent:
    signup_id: str
    token_hash: str
    expires_at: datetime
    attempts: int = 0
    status: str = "pending"


def can_retry(intent: DeliveryIntent, now: datetime, retryable: bool) -> bool:
    if intent.status in {"delivered", "permanent_failure"}:
        return False
    if now >= intent.expires_at or not retryable:
        return False
    return intent.attempts < 3


intent = DeliveryIntent(
    signup_id="signup-7f2c",
    token_hash="sha256:stored-outside-the-email-log",
    expires_at=datetime.now(timezone.utc) + timedelta(minutes=30),
)
Enter fullscreen mode Exit fullscreen mode

Three attempts is an example policy, not a universal limit. The important constraints are explicit expiry, a bounded retry count, and a status machine owned by the application. A provider's event vocabulary can be mapped into those states, but it should not silently become your business logic.

The message should contain one purpose: verify the account. The landing endpoint must check the token server-side, enforce expiry, and make successful consumption idempotent. An expired link is a user-facing state, not a reason to expose the token or the raw delivery response.

How should services be compared without trusting a feature list?

Start with evidence that changes an operational decision. Ask whether the service exposes an accepted-message identifier, distinguishes transient from permanent failure, documents webhook authenticity, and provides a retry or replay model that your team can reason about. Ask how suppression is represented, because repeatedly sending to a known-invalid address is a reliability defect with a delayed symptom.

Then test the boundary under conditions that resemble signup traffic. Send known test addresses, force application timeouts, delay webhook consumption, and replay the same event. Measure whether your own state machine remains correct when events arrive late or twice. A clean dashboard cannot compensate for an ambiguous contract between the send call and the event stream.

Decision criterion Evidence to request Failure it prevents
Acceptance semantics A documented response and stable message identifier Mistaking a request timeout for a definitive rejection
Event delivery Signed or otherwise verifiable callbacks, with replay guidance Trusting forged or silently lost status updates
Suppression behavior Clear permanent-failure and complaint semantics Repeating delivery to recipients who should be excluded
Portability Generic message data and exportable event history Making signup recovery depend on proprietary state
Operational limits Published quotas and throttling behavior Turning a traffic burst into a queue collapse

This is also where three common service categories diverge: a basic SMTP relay may offer broad protocol compatibility but less structured event evidence; a transactional API may expose richer events while creating a tighter request contract; an in-house relay can maximize control while making reputation, queueing, and abuse handling your team’s responsibility. None is automatically the right answer. The decision depends on which boundary your team can operate honestly.

What should the reliability test prove?

A useful test does not stop at a successful 202-style response. It verifies the entire timeline: intent creation, outbound acceptance, callback authentication, duplicate event handling, permanent rejection, delayed delivery, token expiry, and support lookup. The test data should include an address that rejects, an address that receives slowly, and a repeated callback.

I would record a small set of invariants:

  • No accepted signup can be activated without a valid, unexpired token.
  • A repeated callback cannot create a second account transition.
  • A temporary delivery error can be retried without changing the token unexpectedly.
  • A permanent failure produces a clear user action without exposing internal provider details.
  • A support lookup can distinguish “request accepted,” “delivery event received,” and “no confirmed delivery.”

The last invariant matters more than a green send-rate chart. Delivery evidence is incomplete by nature: an event can describe what a boundary observed, not what a person saw in an inbox. State that limitation directly in the product and in incident notes.

The decision I would carry into production

Choose the service whose failure semantics, event authenticity, throttling behavior, and data export fit the state machine you are prepared to own. Keep account verification authoritative in your database, keep the outbound adapter narrow, and make retention deliberate: enough history to diagnose reliability, not an archive of every message forever.

That produces a quieter system. A user gets a link or a useful next step; an operator gets evidence with defined limits; and changing delivery services does not require rewriting account activation. The service is replaceable because the signup state is not outsourced.

Further reading

Top comments (0)