Short answer: choose the email API whose evidence, retry behavior, and deletion controls explain one welcome message from contact form to support queue without retaining more health data than the workflow needs. Personalization is a constrained data projection. It is not permission to copy the entire request into an email.
That boundary matters.
Start with the retention bill, not the send button
The bill is made of retained records, delivery events, rendered content, attachments, logs, and retries. For this workflow, the dominant term is usually event history: one accepted message can create several transitions, and a long retention window multiplies them. Measure submissions per day, events per submission, bytes per event, and retention days before comparing APIs.
def retained_event_bytes(submissions_per_day, events_per_submission, bytes_per_event, retention_days): return submissions_per_day * events_per_submission * bytes_per_event * retention_days print(retained_event_bytes(2500, 8, 900, 30)) Those are planning inputs, not a benchmark or price claim. The multiplication matters. Shortening the event window changes the dominant term directly, while removing a few bytes from a welcome template may not. Keep raw form text in the case-management boundary. Pass the worker a case identifier and the minimum routing attributes.
That choice has a cost. An investigator cannot reconstruct every original field from the mail record alone. The remedy is an authorized lookup, not a second permanent copy in delivery logs, queues, and exports. Consider a submission that contains a patient’s free-text description, an address, a preferred callback window, and a routing topic. The welcome message needs the case identifier and a neutral queue label; the worker may need the callback window; the case system owns the rest. Copying every field into the intent feels convenient when a support agent is debugging a retry, but it creates another record to secure, export, redact, and delete. I deliberately stop retaining full form payloads in message metadata. Convenience during one investigation is a poor reason to replicate sensitive data.
Retention needs separate clocks. A rendered message may need a shorter lifetime than an aggregate delivery metric; an audit decision may outlive the body. Name the record, purpose, owner, and deletion event. “Keep it for compliance” is not a policy.
What must the API prove before you trust it?
I would ask for evidence at four boundaries: acceptance, handoff, provider response, and final delivery status. An HTTP success response can establish that an API accepted a request. It does not establish mailbox delivery, correct queue assignment, or valid personalization.
Create an internal message intent before calling the transport. Store an idempotency key, template version, queue decision, and redacted variables. The adapter receives no more data than it needs.
from dataclasses import dataclass from hashlib import sha256 import json @dataclass(frozen=True) class MessageIntent: case_id: str recipient: str template_version: str support_queue: str variables: dict[str, str] def stable_key(intent: MessageIntent) -> str: material = { "case_id": intent.case_id, "template_version": intent.template_version, "support_queue": intent.support_queue, } encoded = json.dumps(material, sort_keys=True).encode("utf-8") return sha256(encoded).hexdigest() This is intentionally boring. Excluding mutable free text means a punctuation correction does not create a new retry identity. A deterministic key helps only when the receiving boundary honors idempotency or the application enforces uniqueness.
If the form suggests urgent clinical risk, the welcome email is not the escalation channel. Select the designated urgent queue and record that decision. If the topic is unknown, quarantine the intent for review. Do not fall back to a general mailbox because the API call succeeded.
Three paths, three failure stories
Immediate delivery is simple and fast, but a timeout after remote acceptance is ambiguous. Retrying without an idempotency contract can duplicate the welcome email.
Durable handoff stores the intent in an application-owned queue and lets a worker send it. It adds a component, yet provides a place to retry, pause, and inspect. It is useful only when the queue, worker lease, dead-letter policy, and reconciliation job are tested together. A queue by itself is not durability.
Manual review handles uncertain routing, malformed personalization, and policy-sensitive subjects. It costs staff time and is slower, but it stops a transport adapter from turning missing data into a confident-looking email.
| Path | Strong property | Failure to test | Retention consequence |
|---|---|---|---|
| Immediate | Low latency | Timeout after acceptance | Sparse, short-lived intent |
| Durable handoff | Retry boundary | Lease expiry, duplicate worker | Queue records need deletion rules |
| Manual review | Human decision | Stale or missed review | Rationale may outlive message content |
Run one corpus through all paths: valid contact, missing queue, malformed address, provider timeout, accepted-then-lost response, duplicate worker claim, bounce, and deletion before retry. Record state transitions, not only status codes.
Build authentication and personalization as separate proofs
A personalized email has two correctness problems. The message must render for the recipient, and the sending domain must publish an authentication policy. SPF describes authorization for hosts using a domain in the envelope-from identity; it does not prove that a template variable or support-queue decision is correct. DKIM and DMARC address other parts of sender authentication.
def render_welcome(intent: MessageIntent) -> str: name = intent.variables.get("first_name", "there").strip() or "there" case_id = intent.variables["case_id"] return ( "Hi {},\n\n" "We received your request ({}) and routed it to {}.\n" ).format(name, case_id, intent.support_queue) Use separate deployment gates. Test domain authentication, personalization with synthetic cases, and routing independently. The renderer should expose a queue label and case identifier, never a diagnosis, insurance number, or free-text narrative.
Measure the boundary, then choose an API
Ask each candidate whether it provides a stable message identifier, distinguishes accepted from rejected, exposes delivery events, authenticates webhooks, tolerates replay, and supports deletion or export procedures matching your retention policy. If an answer depends on a region or account setting, record that dependency instead of making a generic checkmark.
A worker should back off on transient failures, stop retrying permanent address failures, and send exhausted intents to a reviewable dead-letter state. Webhook consumers must tolerate reordering and deduplicate event identifiers. Store a typed summary instead of every event body when the full body is unnecessary.
A dashboard showing 99.9% accepted requests can coexist with duplicate sends, missing queue assignments, and an unbounded event table. Measure valid intents reaching the right queue, duplicate rate, time to accepted handoff, time to final status, and unresolved ambiguous states. The suitable API is the one whose observable behavior and data controls match those tests.
A limitation is unavoidable: no transport API can repair an ambiguous local state, choose a clinically correct queue, or replace an owned case-management record. If those are the dominant requirements, keep the decision and source data in the application boundary and treat email as a narrow notification channel.
Conclusion: retain evidence, not replicas
Choose the path that proves routing and delivery without making the mail system a shadow case database. Store decisions, identifiers, timestamps, template version, and final status. Retrieve sensitive source data through its owning system when an authorized investigation needs it. Retaining less content can slow an investigation, but it reduces the number of places that must be protected and deleted.



