Did the customer get the receipt? If your app sends mail and keeps no record of each message, nobody can say. Transactional email best practices fix that with a dedicated provider on your own domain and a stored record for every send. Postmark and Resend return an id for each send, and Postmark, Resend and Amazon SES can report delivery events you can keep.

Transactional email best practices start with what a transactional email is, and which ones your app sends

Transactional email best practices for an app come down to four things: a dedicated provider on your own domain, an inventory of the messages that must arrive, a stored provider message id for every send, and a delivery event that updates that record. A transactional email is one that a person’s own action or account state triggers.

US email law draws the same line from the other side: the FTC’s guide treats content that facilitates a transaction the person already agreed to, or updates them about an ongoing one, as transactional, and the next section quotes its wording in full. Authentication, bounce handling and inbox placement sit beside these four, as the rest of how to improve email deliverability for an app.

In a real app, transactional emails are the messages your code sends when something happens to one account. The inventory below is the set of transactional email examples I would expect in a small SaaS with signup, billing and a team feature. Read each row for two things: how fast the message has to land, and who finds out when it doesn’t.

MessageTrigger in the codeMust arrive withinWho notices when it failsStream
Signup confirmation or verificationAccount created, address not yet confirmedSecondsThe user, at once: they cannot get inTransactional
Password resetReset requested on the login pageSecondsThe user, at once, then your support inboxTransactional
Magic link or one-time codePasswordless sign-in or a second factorSecondsThe user, at onceTransactional
Email change noticeAddress changed in settingsMinutesNobody, unless the change was not theirsTransactional
Team invitationOwner invites a teammateMinutesThe owner, days later, when the invitee asksTransactional
Payment receiptPayment provider’s success webhookMinutesThe customer’s accountant, weeks laterTransactional
Failed payment noticePayment provider’s failure webhookMinutesNobody, until the account lapsesTransactional
InvoiceInvoice finalized by the billing systemMinutesThe customer’s finance team, at month endTransactional
Trial endingScheduled job a few days before trial endHoursNobody, until churn shows upTransactional
Plan change confirmationUpgrade or downgrade savedMinutesThe customer, at the next invoiceTransactional
Export readyBackground export job finishedMinutesThe user who asked, at onceTransactional
Security alert: new device or password changedSign-in from a new device, or a password changeSecondsNobody, which is the dangerTransactional
Operator alert to the founderFailure threshold crossed in the appSecondsYou, when a customer tells you firstAlert channel, not a customer stream

The “must arrive within” column gives my rough targets: seconds for anything that blocks a sign-in, such as a reset or a code, and minutes for a receipt. The column that matters more is the fourth. Four rows say “nobody”, and a message that nobody misses for weeks is the one that needs a record most. The last three columns turn these transactional emails from examples into something you can test.

As a transactional email launch checklist, the four practices above read like this, one line each:

  1. One dedicated provider on a domain you own, authenticated, sending only account and transaction mail.
  2. An inventory like the table above, kept in the repository and updated when a new message type ships.
  3. One stored row per send, holding the provider’s message id.
  4. Delivery events from the provider updating that row, so “did it arrive” has an answer per message.

Content matters less than delivery here, but four habits keep these messages useful, and I’d treat them as my own advice rather than a standard. Use a real reply-to that someone reads, because customers reply to receipts and resets with questions. Write a subject that says what the message is (“Your password reset link”), so it is findable in a search a week later. Send a plain text part beside the HTML, so the message still reads where HTML is off. Put the action or the key fact first, and keep promotion out of a critical message. Google’s sender guidelines say the same about receipts in plainer words: “don’t include promotions in sales receipt messages.”

In the Production Hardening Sprint, deliverable 11.2, Transactional delivery tracking, is on the published list for one stated reason: “Untracked failures make it difficult to know whether a customer received a critical message.”

Under the US CAN-SPAM Act, as the FTC’s guide defines them, commercial content “advertises or promotes a commercial product or service”, while transactional or relationship content “facilitates an already agreed-upon transaction or updates a customer about an ongoing transaction”. A message with only the second may not contain false or misleading routing information but is otherwise exempt from most provisions.

Jurisdiction first: CAN-SPAM is United States federal law, and the FTC’s CAN-SPAM compliance guide is the source for everything in this section. What most people call marketing email is, in the law’s words, commercial content; that mapping is my framing. The guide lists five categories of transactional or relationship content, in this order: content that facilitates, completes or confirms a transaction the recipient already agreed to; warranty, recall, safety or security information about something they bought; notice of a change in the terms, features or the recipient’s standing in a membership, subscription, account or other ongoing relationship, or regular account balance information; information about an employment relationship or employee benefits; and delivery of goods or services under a transaction they already agreed to. The guide adds a warning worth reading twice: “the law views these categories narrowly.” The guide’s third kind of content, “other”, is in the FAQ at the end.

Most confusion over transactional vs commercial email comes from mixed messages, such as a receipt with an offer at the bottom. The FTC’s test: if a recipient reasonably interpreting the subject line would likely conclude the message contains an advertisement or promotion, or if the transactional or relationship content does not appear mainly at the beginning, the message is commercial. The rule text says “in whole or in substantial part, at the beginning of the body of the message” in the primary-purpose rule, 16 CFR 316.3.

QuestionTransactional or relationshipMarketing (commercial)
Needs prior consent?Not among the guide’s requirementsNot among the guide’s requirements; the guide requires a clear way to opt out
Needs an unsubscribe link?No: exempt from most provisions, but routing information must not be false or misleadingYes: a clear and conspicuous way to opt out of future marketing email
Can carry a promotion?Only if the subject line does not read as an ad and the transactional content comes first; otherwise the message is commercialYes, identified as an ad
Goes out if the user unsubscribed from marketing?Yes, in my practice, as long as its primary purpose stays in the five categoriesNo: the guide requires opt-outs to be honored within 10 business days
Which stream sends it?My practice: the transactional streamMy practice: a separate marketing stream

On consent, the guide is specific about one group. For senders that run a subscription service or membership program it says that while “you don’t need to get members’ consent to send them marketing emails”, subscribers and members keep the right to opt out. So the line between transactional emails and marketing emails does not depend on who the recipient is: before sending a message without an unsubscribe link to subscribers or members, the guide says to be sure its primary purpose fits one of the five categories.

As a CAN-SPAM compliance checklist for marketing mail, these are the law’s requirements as the FTC’s guide lists them:

  • No false or misleading header information: From, To, Reply-To and routing information identify who sent the message.
  • No deceptive subject lines: the subject reflects the content.
  • The message is identified as an ad, clearly and conspicuously.
  • The message includes your valid physical postal address.
  • The message explains how to opt out of future marketing email from you.
  • Opt-out requests are honored within 10 business days, and the opt-out mechanism works for at least 30 days after sending.
  • You monitor what others are doing on your behalf.

Separately, and as my own good practice rather than the law: send marketing on its own stream or subdomain, and give people a preference center where they can drop one kind of message without losing the rest. By transactional email marketing I mean promotion placed inside a receipt or account message, and the mixed-message test above decides whether that receipt has turned into commercial mail.

Mailbox providers add their own rules on top. Google’s email sender guidelines say that since February 1, 2024, senders who send more than 5,000 messages per day to Gmail accounts must meet extra requirements, including that “Marketing messages and subscribed messages must support one-click unsubscribe”. The page uses the word “transactional” nowhere, and its one-click unsubscribe requirement names marketing and subscribed messages only. Other countries apply their own consent rules to marketing email, and this page covers United States law only. None of this is legal advice.

What goes wrong without it

What the customer saysWhat happenedThe practice that prevents it
”I never got the reset”The message went through a built-in mailer with a low hourly ceiling or a shared sending domain, and the app kept no record of whether it leftA dedicated provider on your own domain, and a row per send
”I paid and got nothing”The receipt was sent from the success page, which the customer closed, not from the payment webhookSend from the payment provider’s webhook
Receipts stop for some customersThe app sends receipts as ordinary campaign or flow emails, and those customers unsubscribed from marketingA transactional feature or provider for account mail
”Why did I get a test email?”Staging shares the production API key and a copy of the production users tableSeparate keys, fake data and a recipient guard in staging
Signup hangs or failsThe email is sent inside the request, and the provider was slowSend off the request path

Each cause above is my estimate of how the failure usually happens, not a count from audits. The first row has a builder-specific version. Supabase’s built-in service and the custom SMTP fix are explained in the two Supabase email rate limits. On Lovable, “Lovable limits how many emails a workspace sends per hour, with separate limits for app emails and authentication emails”: 100 app and 500 authentication emails per hour on Pro, 300 and 3,000 on Business, and 1,000 and 10,000 on Enterprise. The other reasons a confirmation email never arrives in a builder app are in why users can’t log in to your app.

For the second row, the receipt belongs to the code that handles the payment provider’s event, and that handler has to cope with the same event arriving twice; how to make a webhook handler idempotent covers that side. The third row is the subject of the marketing platforms section below, which says which platforms let a transactional message reach people who unsubscribed from marketing, and on which plans. The fourth row is answered in the staging section.

Picture it from the support side. Your app sends password resets through a provider but keeps nothing about what it sent. The provider has a delivery delay for part of a day, customers write in that the reset never came, and nobody can say which resets left, which were delayed and which never went. With a stored message id and the delivery events on each send, you could answer each customer and resend only what failed. I’d call a provider’s bad afternoon survivable; not knowing which messages it swallowed is what turns it into support tickets.

Messages that are sent and still land in the junk folder are a different problem: transactional emails landing in spam has its own causes. So does an error from the provider or the builder itself, such as email rate limit exceeded.

How to send transactional email from an app: the provider, the send path and the record

Sending transactional email from an app takes seven steps: pick a dedicated provider, authenticate the domain, separate the stream from marketing, send from the server with an idempotency key, move the send off the request, store the provider’s message id on a row, and update that row from the provider’s delivery webhooks. The row is the proof.

  1. 01 Choose a dedicated transactional provider (criteria in the next section)
  2. 02 Authenticate the sending domain with SPF, DKIM and DMARC
  3. 03 Put transactional mail on its own stream or subdomain, apart from marketing
  4. 04 Send from the server through the provider API, never from the browser, with an idempotency key
  5. 05 Move the send off the request path when the request does not need it to finish
  6. 06 Store one row per send: user, message type, provider message id, status, timestamps
  7. 07 Consume the provider delivery webhooks and update that row; bounces and complaints suppress the address

Step 2, SPF, DKIM and DMARC setup, is its own job, done once per sending domain. Step 3 should keep a campaign’s complaints off the path a password reset takes. Postmark’s message streams are one vendor’s version: separate transactional and broadcast streams on dedicated IP pools, where “Traffic never mixes”.

Step 4 is where retries go wrong. A send that times out gets retried, and without a key the customer gets two resets. Resend’s idempotency keys are “kept in the system for 24 hours”, and within that window a repeated request with the same key returns the same response without sending again. Where a provider documents no key, the app’s own send row with a unique key does the job: an idempotency key for safe retries kept on your side. Step 5 matters for signups: if the provider is slow, the user should not wait on it, so the send runs as a queued job, the usual answer to how to run long tasks in the background.

Steps 6 and 7 are the record. Each provider names its events in its own docs: Resend sends events such as email.sent, email.delivered, email.bounced and email.complained, Postmark has delivery, bounce and spam complaint webhooks, and Amazon SES sends a delivery notification through Amazon SNS only “If you set up delivery notifications”. A delivered or bounced event updates the row; bounces and complaints then suppress the address, an item on the email bounce handling checklist. A spike in failures, meaning a failure rate over a time window rather than one failed message, should raise an alert, set up as in how to alert on error rate spikes.

Transactional email deliverability for an app rests on these seven steps, and transactional email delivery is what step 7 measures: whether each message reached the recipient’s mail server. That framing is mine. You can send transactional emails without steps 6 and 7, but then nobody can answer a customer about one specific message.

Before writing any code, the provider’s dashboard log answers “did it arrive” for as long as the plan keeps it. SendGrid’s pricing page lists Searchable Email Activity of 3 days on the Free Trial and Essentials plans and 7 days on Pro. Postmark keeps data for 45 days by default, Resend 30 days on Free and Pro, and Mailgun 1 day of logs on Free and Basic and 5 days on Foundation. Amazon SES keeps a searchable copy of sent mail only through its Mail Manager archiving add-on, billed separately, with a default retention period of 180 days and search by From, To and subject; without it, the stored delivery event from step 7 is the record on SES. A log that keeps three days cannot answer a customer who writes in next week; the row in your own database can.

Transactional email service providers: what to compare, and what the free tiers include

Compare transactional email service providers on what they let you look up later, not only on price: delivery webhooks, how many days of message logs are kept, separate streams, a test mode, and the price at your real volume. Free offers differ in kind: a free monthly plan, a 60-day trial, or new-customer credits. Read each free-tier cell.

For an app sending hundreds to low thousands of messages a month, my criteria are, in order: delivery events by webhook; how long message logs are kept, which decides whether you can answer a customer next week; separate streams; a test mode; an SDK for your stack; and the price at your volume. The table covers five providers in alphabetical order, each cell from the provider’s own pricing and docs pages.

ProviderFree tierFirst paid tierLog retentionSeparate streamsDelivery webhooksSandbox or test mode
Amazon SESNo email allowance; new AWS customers get up to $200 in AWS Free Tier creditsEssentials: $0.16 per 1,000 emails up to 10M a month, no per-month plan fee printedMail Manager archiving add-on, billed separately: 180 days by default, searchable by recipientDedicated IP pools, such as one for marketing and one for transactional mail (dedicated IPs are paid)Delivery and bounce notifications through Amazon SNS, if you set them upMailbox simulator, billed at the outbound rate; new accounts start in the SES sandbox
Mailgun$0/mo, 100 emails a dayBasic: from $15/mo, 10,000 emails a month1 day (Free, Basic), 5 days (Foundation)Separate IPs for bulk and transactional mail advised at high volume; dedicated IP pools on Scale”Tracking, analytics, and webhooks” listed on every planTest mode: accepted, not sent, and charged
PostmarkFree plan: 100 emails a month, never expiresBasic: $15.00/mo for 10,000 emails, $1.80 per extra 1,00045 days by defaultTransactional and broadcast streams on separate IP poolsEvent webhooks on every planSandbox servers (count toward volume); POSTMARK_API_TEST token
Resend$0/mo, 3,000 emails a month, 100 a dayPro: $20/mo for 50,000 emails, $0.90 per extra 1,00030 days (Free, Pro)Separate subdomains per sending purpose, advised in its docs1 endpoint on Free, 5 on Pro, all eventsTest addresses at resend.dev (count against quota)
SendGridFree trial: 100 emails a day for 60 daysEssentials: from $19.95/moSearchable Email Activity: 3 days (Free Trial, Essentials), 7 days (Pro)IP pools for transactional and marketing mail, on dedicated IPs (Pro and Premier); subusers on Pro and Premier1 event webhook on Free Trial, 2 on Essentials, 5 on ProSandbox mode: validates only, no events

Prices checked 2026-10-04 on each provider’s pricing page: Postmark pricing, Resend pricing, SendGrid pricing on Twilio’s site, Amazon SES pricing and Mailgun’s pricing page.

Read the free-tier column in each provider’s own words, because the offers differ in kind. Postmark’s free Developer plan “includes 100 emails every month” and “never expires or runs out”. SendGrid’s “Free trial lets you send 100 emails/day for 60 days”. Resend prints Free at “$0/mo” for “3,000” emails a month and “100 emails a day”. Amazon SES prints per-1,000 rates and new AWS customers’ Free Tier credits of up to $200, not an email allowance. Every new SES account also starts in the Amazon SES sandbox, where “You can only send mail to verified email addresses and domains, or to the Amazon SES mailbox simulator”, with a maximum of 200 messages per 24-hour period, until AWS grants a request for production access; the sandbox status is set per AWS Region. That account sandbox is a sending restriction, not a test mode. Four of the five let you send transactional emails free within the limits their own cells print, and Amazon SES gives new AWS customers credits of up to $200 instead; none of them is a free transactional email service at any volume.

What those tiers cost month by month, and why an app that sends in bursts hits Resend’s daily ceiling first, is worked through in what an email API costs per month. I don’t rank these as the top transactional email services: I ran no deliverability tests, so the table compares what each provider documents and names no winner.

Builder-bundled email counts as a transactional email delivery service when it gives you the log and the events. Lovable’s emails “are available on paid plans” and need a domain you own with access to its DNS settings. Each paid workspace includes 50,000 transactional emails per month. Its docs describe an analytics and logs view for the connected email domain with Sent, Delivered and Bounced counts, and an email activity log showing recipient, status, type and time. Whether the app itself can receive those delivery events by webhook is not stated in Lovable’s docs. Bundled email like this is enough to start if that activity log answers your support questions; if it doesn’t, the record has to live in your own database.

Sending from a Next.js app: npm resend, React Email and the open alternatives

The resend npm package is the Node.js SDK for Resend’s email API. In a Next.js app it belongs in a route handler or server action, with the API key in a server-only variable. One call sends the message and returns an id, which the app should store with the user.

Resend’s Node.js guide lists two prerequisites, “A Resend API key” and “A verified domain”, and installs the SDK with npm install resend. The guide stores the key as RESEND_API_KEY in an environment file. My rule on top: the call runs in a route handler or server action, never in a client component, and the variable never gets a NEXT_PUBLIC_ prefix, so the key never reaches the browser. The block below follows Resend’s documented call and the idempotency option from its docs; it is a sketch to adapt, not tested output, and db stands for your own data layer.

// Server only: a route handler or server action. Follows Resend's Node.js docs.
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);

const { data, error } = await resend.emails.send(
  { from: 'Billing <billing@mail.example.com>', to: [user.email], subject: 'Your receipt', html },
  { idempotencyKey: `receipt/${invoice.id}` },
);
if (error) throw error;
await db.emailSends.insert({ userId: user.id, type: 'receipt', providerId: data.id, status: 'sent' });

React Email is for building and sending emails “using React and TypeScript”. To use it “with any email service provider”, its docs say you convert the components “into a HTML string” with the render utility, and the page lists integrations for Resend, Nodemailer, Mailgun, SendGrid, Postmark and AWS SES, among others. Its footer reads “Brought to you by Resend, MIT License.” Resend’s open source page says React Email is “Created and maintained by our team”.

Whether Resend is open source depends on which part you mean. In that page’s words, “Resend is built on open source technologies”, and it lists React Email, its “9 official open source SDKs” and tools such as an MCP server and a webhooks ingester. That the sending service itself is open source is not stated there. The plain open source alternative to Resend’s SDK is Nodemailer, which sends “from Node.js” over SMTP and other transports, so I’d expect it to keep the app portable across any provider with SMTP. Self-hosting the sending side as well is possible, but then you own IP reputation, which is the job you were paying a provider to do.

Transactional emails in HubSpot, Klaviyo, Mailchimp, Brevo and Marketing Cloud

Marketing platforms send transactional email through a separate feature: HubSpot’s add-on needs Marketing Hub Professional or Enterprise, Klaviyo flags single emails in metric-triggered flows on paid accounts once approved, Mailchimp Transactional’s full version needs a Standard or Premium account, and Brevo has a transactional API. The test: such messages must still reach customers who unsubscribed from marketing.

For all five platforms the question is the same: can the campaign tool you already pay for send your receipts and resets? The table answers it from each vendor’s own docs.

PlatformSends transactional mail?HowPlan or account conditionReaches contacts who unsubscribed from marketing?Docs checked on
HubSpotYes, with the Transactional Email Add-OnSingle-send API or SMTP API, on a dedicated IPMarketing Hub Professional or Enterprise; the add-on is bought through your Customer Success Manager or sales representativeYes, “regardless of subscription preferences”; contacts with a previous unknown-user bounce are dropped2026-10-04
KlaviyoYes, as single flow emails marked transactionalPer-email transactional status, approved by KlaviyoPaid accounts only; metric-triggered flows only; approval can take up to 24 hoursSuppressed profiles still receive them, except after 7 consecutive soft bounces, a hard bounce or a spam complaint2026-10-04
MailchimpYes, through Mailchimp Transactional (original name Mandrill)A separate product with its own Transactional API or SMTPFull version needs a Standard or Premium Mailchimp accountIts help page says transactional recipients don’t need to be subscribed contacts2026-10-04
BrevoYesTransactional email API and SMTP relayAll plans, per Brevo’s pricing pageNot stated on Brevo’s API or pricing pages2026-10-04
Salesforce Marketing CloudYes, through the Transactional Messaging APIREST API with send status and event notificationsNot stated in Salesforce’s Trailhead moduleNot stated in Salesforce’s Trailhead module2026-10-04

On transactional emails in HubSpot, HubSpot’s transactional email guide is clear that these messages can be sent “regardless of subscription preferences, quarantine status, previous bounces, or non-marketing contact status”. Klaviyo transactional emails work differently: Klaviyo’s guide to transactional flow emails says the status is “marked on a per-email basis, not per-flow”, and an edit removes it until Klaviyo approves the message again. Mailchimp transactional email is a separate product, and the docs for Mailchimp Transactional still make “the occasional reference to the product’s original name, Mandrill”; it sends through its own API or SMTP, and you buy it in blocks of emails each month. The Brevo transactional emails API is a single endpoint in Brevo’s transactional email API docs, which describe these messages as “automated, non-promotional messages triggered by user actions”. For transactional email in Marketing Cloud, Salesforce’s Trailhead module says each message can be tracked “using the messageKey attribute”, and that “Real-Time send event notifications are only available on the Transactional Messaging API.”

For an app, I’d send account and payment messages through an API you call from your server and can query by message id. If the marketing platform offers that, and its docs say those messages still reach contacts who unsubscribed from marketing, it can do the job; if it only offers campaign automation, it cannot. On top of that, password resets and one-time codes go through the same provider, and land in the same log, as the rest of the account mail, so one place answers “did it arrive”.

The messages themselves: templates, receipts, inline images, and which events earn an email

Once the send path keeps a record, the content of each message decides whether the reader can act on it.

Transactional email templates: where the HTML comes from, and what to change

Transactional email templates are safer to adapt than to write, because email HTML has to survive mail clients that render it differently. The usual sources are the provider’s own template library, React Email’s components, MJML, and the open repositories those projects publish. Change the sender, the single action, the plain text part and the footer.

SourceWhat you getLicense or termsWhat to change
Postmark’s template libraryTen templates, from Welcome and Reset password to Receipt, Invoice and Dunning”Free for commercial use under the MIT License.”Sender, action link, plain text part, footer
Mailgun’s templates featureTemplates stored in Mailgun and sent by name or template ID; up to 100 account-level templates with 40 versions eachPart of the Mailgun accountPlaceholders and the version your code references
Mailgun’s template repository on GitHub”Responsive transactional HTML email templates”MIT LicenseEverything above, plus your own branding
React EmailReact and TypeScript components rendered to HTML”MIT License”, per its footerComponent props and copy
MJMLA markup language and an “open-source engine” that outputs responsive HTMLMIT License, per its repositoryThe markup source, then rebuild

Postmark’s transactional email templates are the closest thing to a ready set for a SaaS: Postmark email templates cover welcome, reset, invitation, receipt, invoice, trial and failed-payment messages. The Mailgun email templates come in two forms: Mailgun’s templates feature stores them on Mailgun’s side, and a separate repository holds open HTML files. MJML is “a markup language designed to reduce the pain of coding a responsive email”. Among transactional email templates on GitHub, Mailgun’s repository in the table is one, and Postmark offers its own set “as Open Source”, in its words.

What to change in any template, as my list: the sender name and reply-to, the one action, the plain text part, the footer’s company name and address, the preheader, and every placeholder. What not to add: a promotion. Then pick one home for the template, either the repository with version control or the provider’s template store with an id the code references, and not both. The verification email needs a verification email template written for that one job, and the verification and reset flows around it come down to how to send a verification email.

Payment successful email template, and payment confirmation email best practices for the sender

A payment successful email is sent when the payment provider’s webhook confirms the charge, never from the success page. I give it 9 fields: item, amount and currency, date, payment method label, receipt number, billing period, seller name and address, a billing portal link and a monitored reply-to. If Stripe already sends receipts, send no duplicate.

The design of a payment successful email template matters less than the sender’s side: what triggers it, what it must contain, and how you know it went. The trigger is the payment provider’s webhook event, not the success page, for the reason in the second row of the failure table above. My payment confirmation email best practices start with these nine fields:

  • What was bought: the plan or product name.
  • The amount and the currency.
  • The date of the charge.
  • The payment method label, such as the card brand and last four digits.
  • The invoice or receipt number.
  • The billing period, for a subscription.
  • The seller’s legal name and address.
  • A link to the billing portal.
  • A reply-to address that someone monitors.

Stripe can send receipts itself. Per Stripe’s email receipts docs, the setting is in the Dashboard under Settings > Business > Customer emails, where you toggle “Successful payments” or “Refunds”, and “receipts are only sent if the payment succeeds.” If that setting is on, my rule is not to send a second receipt from the app: decide which system owns the receipt and record it in the inventory. Stripe’s page also says “If you need to send a receipt for a test payment, send a manual receipt”, so a test payment does not show whether the automatic receipt works. A failed payment notice is a separate message with its own inventory row. What a tax invoice must contain varies by country and is out of scope here.

Inline attachment or hosted image: what inline images in email are

An inline attachment is an image attached to the message and shown in the body through a cid reference to its Content-ID. The alternative is a hosted image the email loads by URL. Either way, nothing the reader needs, such as an amount, a code or a link, belongs inside an image.

An inline image in email is one that displays in the body of the message rather than as a file at the bottom. An inline attachment email does that by attaching the file and pointing to it from the HTML: RFC 2392 defines the “cid:” URL scheme “to permit the HTML to refer to the images or other data included in the message”. Resend’s docs show the provider side: put cid:logo-image in the image’s src, and give the attachment a matching contentId, an arbitrary string under 128 characters.

MethodHow it worksWhat to watch
Hosted image URLThe HTML loads the image from your server or CDN when the message is openedIn practice, a client that blocks remote images shows nothing until the reader allows it
Inline attachment by Content-IDThe image travels inside the message and the HTML points to it with a cid: URLEvery send carries the file’s weight, and some clients, I believe, also list it as an attachment

For transactional mail my rule is simple. A logo can be hosted or sent as one of your inline attachments, while the code, the amount and the link always stay as text.

Email notification best practices: which events earn an email, and how often

Email notification best practices start with one test: send an email only when the user must act or must have a record, and cannot be assumed to be looking at the app. Security and money events always send at once. Activity by other people is batched behind a preference.

Event classEmail or in-appFrequency rule
Security and account eventsEmailAlways, at once, no opt-out
Money eventsEmailAlways, at once
Things the user asked for (an export, a report)EmailOnce, when ready
Activity by others (comments, mentions)In-app, with an optional emailBatched, with a per-type preference
System healthThe operator’s alert channelNever to a customer list

This is my rule, not a standard. One notification per event per user, enforced by the idempotency key from the send path, stops duplicates when a job retries. A preference page covers the optional classes, and lists the mandatory ones as always on so nobody hunts for a switch that does not exist.

Staging, test sends and QA before anything reaches a customer

Two checks happen before a message type goes live: staging that cannot reach a customer, and a QA pass on each message.

SendGrid staging environment, sandbox mode and a catch-all inbox: never mail a real customer from a test

A staging environment must be unable to reach a customer’s inbox. It takes five rules: a separate API key, the provider’s test mode or a catch-all inbox, a code guard that refuses recipients outside an allowlisted domain, seed data with no real addresses, and the environment name in every subject line. SendGrid’s sandbox mode validates a send without delivering it.

What staging must keep separate in general, from credentials to seed data, is covered in one database and no staging; this section keeps only the email rules. These are the rules I follow:

  1. 01 A separate API key per environment, and a separate provider account, subuser or server for staging where the plan offers one
  2. 02 Staging sends only in the provider test mode or to a catch-all inbox
  3. 03 A code guard that rewrites or refuses any recipient outside an allowlisted domain when the environment is not production
  4. 04 Staging data holds no real customer addresses
  5. 05 The subject line carries the environment name

Rule 1 builds on separate environment variables for staging and production. SendGrid’s pricing page lists subuser management on its Pro and Premier plans only, and Postmark’s pricing page lists servers on every plan, up to 10 on the free plan.

Rule 2 depends on what each provider’s test mode really does:

  • SendGrid’s sandbox mode “validates a Mail Send request without delivering the email”, “is only used to validate your request”, and requests in it “will not generate events in either Event Webhook or Email Activity”.
  • Postmark’s sandbox mode keeps test messages out of real inboxes and is a place to “experiment with webhooks”, but its messages “do count towards your monthly sending volume”. Its test token, “passing POSTMARK_API_TEST as your server API token”, sends test emails “that won’t actually get delivered”.
  • Resend’s test addresses (delivered@, bounced@, complained@ and suppressed@resend.dev) “count against your account’s sending quota”.
  • Amazon SES’s mailbox simulator addresses do not count toward the sending quota or the bounce and complaint rates, but its pricing page says simulator emails are “billed at the same rate as Outbound Email”.
  • Mailgun’s test mode accepts the message but will not send it, and “You are charged for messages sent in test mode!”

An end-to-end test of the webhook and the send row uses a provider’s documented test addresses: Postmark’s bounce-testing addresses, the SES simulator or Resend’s test addresses. SendGrid’s sandbox mode cannot do it, because it produces no events. A catch-all inbox is my suggestion for looking at real rendering. Rule 4 is covered in how to generate realistic fake data for staging. The same five rules protect a SendGrid staging environment and a staging setup on any other provider; only the name of the test mode changes.

Email quality assurance checklist for every automated message

An email quality assurance checklist for automated messages has 10 checks, run once per message type and again when a template changes: merge fields with real and empty data, every link, link expiry, the plain text part, sender and reply-to, subject and preheader, rendering in three clients, dark mode, no promotion, and the inventory entry.

These are my checks:

  • Every merge field renders with real data and with empty data, so no message ever opens with “Hi ,”.
  • Every link resolves, uses the production domain and carries no staging host.
  • The action link expires as the inventory says and works only once.
  • The plain text part exists and reads on its own.
  • From name, From address and reply-to are right, and someone reads the reply-to.
  • The subject and preheader say what the message is.
  • The message renders in Gmail, Outlook and Apple Mail, on a phone and on a desktop, checked by sending to real mailboxes.
  • Dark mode does not hide the logo or the button.
  • No promotion appears in a mandatory message.
  • The message type is in the inventory, with its stream.

Rendering tools can speed up the client check, but a real mailbox on each client is the evidence. Keep the results next to the inventory, one line per message type with the date.

How to verify it

Transactional delivery is verified by triggering every critical message type from production to two mailbox providers, then matching three records for each: the provider’s record with a delivered event, the message in the mailbox, and the app’s own row with the same message id. A forced bounce must show up in the row too.

The goal is to test every automated email is delivered, not only the one you happened to notice. These steps run against the live app with test accounts you control:

  1. 01 Take the inventory and trigger every critical message type from production, using test accounts on two different mailbox providers
  2. 02 For each send, find the provider record by recipient the same day and note the message id and the delivered event
  3. 03 Confirm the message is in each mailbox, and note inbox or spam
  4. 04 Confirm the app row for that send holds the same message id and the delivered status
  5. 05 Force one failure by sending to a documented bounce test address, then check the provider record, the app row and the suppression

Step 2 works only inside the plan’s log retention window, as the send path section says; on Amazon SES without the archiving add-on, the stored delivery event is the provider record. Step 4 is the one that proves the webhook works. For step 5, the documented addresses are Postmark’s at bounce-testing.postmarkapp.com, such as hardbounce@, Amazon SES’s bounce@simulator.amazonses.com, and Resend’s bounced@resend.dev. After the send, the provider’s record shows a bounce for that message id: its log, or on SES the bounce notification, which SES sends “depending on how you set up your account”. The app’s row shows the bounce event, and if the bounce handling from step 7 of the send path is in place, the app has marked the address as suppressed.

SendGrid’s sandbox mode produces no events and Mailgun’s test mode does not send, so neither can run step 5, and neither provider’s pricing or docs pages cited here name a bounce test address. On those two, record step 5 as not run, with that reason, and never point it at a made-up address: Resend’s own test page warns against “sending to fake email addresses”.

Message typeTriggered (time)Provider record (message id, event)Delivered to mailbox ADelivered to mailbox BApp row status
Password resetTime sentId and delivered eventInbox or spamInbox or spamSame id, delivered
Signup confirmationTime sentId and delivered eventInbox or spamInbox or spamSame id, delivered
Payment receiptTime sentId and delivered eventInbox or spamInbox or spamSame id, delivered
Forced bounceTime sentId and bounce eventNot applicableNot applicableSame id, bounced, address suppressed

Pass is a filled table with no empty cell; any message type with no provider record (its log, or the stored delivery event where step 2 allows it) fails. Evidence to keep: the table, the message ids and the date. The failure alert is not checked here, because one forced bounce is not a spike and a correct alert stays silent; it is fired on purpose with the drill on the alerting page from the send path section, and the evidence is the alert in its channel with its time.

In the sprint, deliverable 11.2 is verified this way: “Trigger each critical message type and confirm provider records and expected recipient delivery.”

Inbox placement scoring and seed-list tests are a separate check, part of how to test email deliverability.

Where the sprint does this

In the sprint, deliverable 11.2 is where we “Configure a dedicated transactional provider with delivery tracking for account and transaction messages”, verified by the line under How to verify it. Deliverable 11.1 verifies and configures SPF, DKIM and DMARC for the transactional sending domain, and 11.3 processes bounces and complaints and suppresses further sending where appropriate. The result goes into the production readiness report, deliverable 13.1, which delivers the result for every scope item, the work completed and its verification evidence, accounts for all 123 IDs, keeps failures visible until resolved and explains genuine non-applicable items. Legal advice and certification are separate services. Hosting, paid tools, and API usage remain in your accounts. All three deliverables are listed in the email checks in the published scope.

Common questions about email your app sends

What are the three types of emails?

Commercial, transactional or relationship, and other: those are the three types of information an email can contain, in the FTC’s guide to the United States CAN-SPAM Act, where other content “is neither commercial nor transactional or relationship”. The message’s primary purpose decides which rules apply. The first two are defined in the transactional versus marketing section above. Marketers’ own three-way splits vary; the law’s split is the one that changes what you must do.

Can postmark receive emails?

Yes. Postmark’s inbound processing accepts email sent to an address it gives you and delivers it “to you via a webhook in nicely formatted JSON”, which its docs describe as useful when people “send or reply to your outbound emails”. Its pricing page marks inbound email on the Free, Pro and Platform plans, not on Basic. An app needs it for reply-by-email features, such as answering a comment or a support ticket from the inbox; Postmark inbound processing has the setup.

Can I get a free SMTP server?

Yes, from a provider rather than as a server you run. Postmark’s, Resend’s and SendGrid’s pricing tables mark SMTP on their free offers, and Mailgun’s free plan lists “RESTful email APIs and SMTP relay”. Those free offers are not the same kind of thing, as the providers section explains, so read each one’s free-tier cell in the table before you rely on it. My rule: a personal mailbox’s SMTP login is not for app mail, because it ties your app’s messages to a person’s account and its sending limits.

Do spammers know if you open an email?

They can, if the message loads tracking images. Postmark’s docs describe open tracking as “an invisible pixel” that lets Postmark “record information when the email is viewed”; which I’d read as meaning any sender using such a pixel can see an open when the images load. Postmark lists the limits itself: opens are tracked on HTML email only, a client that blocks images and ad blockers can stop the pixel, Gmail’s image proxy still shows the open but hides geography and operating system, and Apple Mail Privacy Protection “can cause false-positive opens to be reported”. For your own transactional mail, my rule is to turn open tracking off on security messages and never treat an open as proof of delivery.