Choose an API-only transactional email service for a new healthtech contact form when the application can poll delivery events; choose Postmark, SendGrid, or Mailgun when SMTP or webhook-first events are already part of the reliability contract.
TL;DR: The easiest setup is the one that preserves evidence from form acceptance through queue ownership. Templates, domain verification, and DKIM cover the first release. They do not make an accepted API request equivalent to a delivered message. For this build, I would make the application route the request, give every submission a stable idempotency key, and treat email as a recorded side effect.
The concrete constraint is unpleasantly simple: a prescription question cannot disappear into a generic inbox. Delivery reliability wins over a shorter quickstart.
Should Postmark, SendGrid, Mailgun, or API-only transactional email handle routing?
A contact form has two jobs that are easy to conflate. It classifies the request, then it asks an email provider to deliver a notification. The application should own the first job. A template, subject-line convention, or mailbox rule should not decide whether a message belongs to billing, account support, engineering, or clinical support.
The useful state machine is small: accepted, routed, submitted, then observed as delivered or failed. Persist the contact-form record before sending. Derive the idempotency key from that record. Store the provider response beside it. If the process dies after the remote service accepts the call but before the local transaction finishes, a retry must reuse the same key.
Acceptance is not delivery.
That distinction changes the vendor comparison. I benchmark time-to-first-traceable-message, not time-to-first-200. The count includes domain work, template review, retry behavior, and event plumbing. A five-minute send demo followed by two days of ambiguous delivery state is not an easy setup.
Four choices, one reliability test
Postmark, SendGrid, Mailgun, and the API-only option can all sit behind a narrow delivery adapter. They shouldn't be treated as interchangeable. Postmark, SendGrid, and Mailgun belong on the shortlist when the surrounding application requires an established SMTP path or webhook-oriented processing; their current documentation is where I would verify the exact contract before committing. Infrai is one plain REST API under one key, covering 295 routes across 20 modules. There is no SDK to install and no client-library version to babysit; anything that can send an HTTP request can call it, in any runtime. Its template create, update, and preview operations cover a basic branded welcome flow, while domain verification and DKIM management cover the initial authentication setup.
The sharp edge is events. This API's email events are pull-based, not pushed through webhooks. Polling means a scheduler, a durable cursor, duplicate-safe processing, and an explicit delay budget. It can be acceptable for an early-stage welcome email or a low-volume internal dashboard. It is a poor match for a support operation whose escalation clock depends on immediate event delivery.
| Option | Sensible reason to test it | Contract to verify first |
|---|---|---|
| Postmark | A dedicated transactional-email candidate | Required SMTP, template, and event behavior in the current docs |
| SendGrid | A broad email-platform candidate with established integrations | The exact surface needed by this small workflow, especially event handling |
| Mailgun | An email-infrastructure candidate for API or SMTP-oriented applications | Current template and event semantics against the application state machine |
| Plain REST service | A new HTTP-native service with basic templates and no SDK lifecycle | Polling tolerance and the absence of SMTP relay |
This is not a feature-score table. It is a rejection test. I first weighted template setup most heavily, then reversed that decision: event evidence controls this healthtech queue. These are real limitations. Infrai is not a fit if an existing system expects SMTP credentials or operations needs webhook-triggered escalation; choose Postmark, SendGrid, or Mailgun instead after verifying the required contract in current documentation. Polling adds enough machinery to erase the setup advantage in those cases. Conversely, installing a large client package doesn't buy reliability by itself.
The smallest implementation I would ship
Keep routing deterministic and delivery isolated. The following TypeScript example maps four form topics locally and performs one write call. It takes the verified request body from INFRAI_EMAIL_BODY because the request fields are discoverable at runtime; freezing unverified provider fields into a tutorial would create confident, broken code.
type Topic = "account" | "billing" | "prescription" | "technical";
type Queue = "accounts" | "billing" | "clinical-support" | "engineering";
type ContactForm = {
id: string;
topic: Topic;
replyTo: string;
message: string;
};
const queueFor: Record<Topic, Queue> = {
account: "accounts",
billing: "billing",
prescription: "clinical-support",
technical: "engineering",
};
function route(form: ContactForm): ContactForm & { queue: Queue } {
const replyTo = form.replyTo.trim();
const message = form.message.trim();
if (!replyTo.includes("@")) throw new Error("A valid reply address is required");
if (message.length === 0) throw new Error("A message is required");
return { ...form, replyTo, message, queue: queueFor[form.topic] };
}
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
const rawBody = process.env.INFRAI_EMAIL_BODY;
if (!apiKey || !baseUrl || !rawBody) {
throw new Error("INFRAI_API_KEY, INFRAI_BASE_URL, and INFRAI_EMAIL_BODY are required");
}
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function send(body: unknown, idempotencyKey: string): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${baseUrl}/email/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 3) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delay = Number.isFinite(retryAfter)
? retryAfter * 1000
: 2 ** attempt * 1000;
await sleep(delay);
continue;
}
const result: unknown = await response.json();
if (!response.ok) {
throw new Error(`Email send failed (${response.status}): ${JSON.stringify(result)}`);
}
return result;
}
throw new Error("Email send retry limit reached");
}
const contact = route({
id: "contact-7f31",
topic: "prescription",
replyTo: "patient@example.com",
message: "My refill status has not changed.",
});
const result = await send(JSON.parse(rawBody), contact.id);
console.log({ id: contact.id, queue: contact.queue, result });
The explicit POST matters. So do the boring parts: status checking, response-body error reporting, bounded retries, and Retry-After. Four attempts prevent a tight loop. Exponential fallback starts at one second. The stable contact ID prevents a timeout retry from producing a second notification under the platform's 24-hour default deduplication window.
I would persist contact-7f31 before this function runs. The literal only makes the example executable. In production, generating a fresh idempotency key inside send would defeat the entire mechanism.
Deliverability begins outside the API call
Domain verification and DKIM are baseline controls. DMARC adds policy and reporting around authenticated mail, and RFC 7489 is the primary reference. Rollout still needs care: authentication configuration is evidence about the sending domain, not a guarantee that a recipient mailbox accepted a particular message.
Tracking opens is weaker evidence. Apple Mail Privacy Protection can download remote content privately and limit what senders learn about Mail activity. A dashboard should not equate an open pixel with a patient reading a response. For queue operations, delivery state, an actual reply, or an explicit in-product acknowledgement is more useful.
There are other hard boundaries. Managed email OTP is unavailable here, so an email verification fallback must be built by the application. Scheduled email has no cancellation operation. A domestic email vendor remains pending and therefore provides no basis for a China-compliance claim. None of these are template problems, and none should be hidden behind an attractive quickstart.
What changes when the queue grows
At low volume, one polling worker can advance a durable event cursor and update stored contact records idempotently. At higher operational stakes, I would define a maximum age for each state, alert on records stuck after submission, and assign an owner to failures. Several uncoordinated cron jobs are not observability.
I would also separate welcome-email reporting from contact-form escalation. A welcome message can often tolerate aggregate, delayed reporting. A prescription support request needs per-message ownership. Same transport, different reliability budget.
The final decision rule is compact. Choose the plain REST path when the service is new, HTTP-native, and polling delay is acceptable. Choose among Postmark, SendGrid, and Mailgun after verifying their current contracts when SMTP compatibility or pushed events are mandatory. Before signing off, run one proof with the real domain and template: submit each of the four topics, force one rejected request, retry one stable ID, and trace the resulting state. Count adapter code and configuration objects. That benchmark exposes glue better than a marketing matrix does.
References
- Postmark developer documentation: https://postmarkapp.com/developer
- SendGrid Email API documentation: https://www.twilio.com/docs/sendgrid/api-reference
- Mailgun documentation: https://documentation.mailgun.com/docs/mailgun
- RFC 7489, Domain-based Message Authentication, Reporting, and Conformance (DMARC): https://datatracker.ietf.org/doc/html/rfc7489
- Apple Mail Privacy Protection guide: https://support.apple.com/guide/iphone/use-mail-privacy-protection-iphf084865c7/ios
Top comments (0)