License Emails That Actually Reach the Inbox
The license key email is the single most important email your business sends. It is the only thing standing between a completed payment and a working product, and it is routinely treated as an afterthought.
When it fails, the failure is silent. The customer does not see an error. You do not see an error. Stripe shows a successful payment, your revenue chart looks fine, and somewhere a person who just paid you is refreshing an empty inbox. A percentage of them will email support. Most will not. They will request a refund or simply write you off.
I have watched this happen to an app that changed sending domains on a Friday. Nothing broke loudly. Purchases kept completing all weekend. The only signal was activation rate, which is why it sits at the top of the licensing metrics worth tracking.
This is the fix list, in priority order.
The three DNS records and what each one actually does
SPF, DKIM, and DMARC are three separate mechanisms that together prove a message claiming to come from your domain really did. Receivers check them on every message. Get them wrong and nothing else you do to the email matters.
SPF (Sender Policy Framework) is a DNS TXT record listing which servers may send mail for your domain. The receiving server checks whether the connecting IP is on that list. It is an allowlist, nothing more. It does not protect the message contents and it breaks on forwarding, because a forwarding server is not on your list.
DKIM (DomainKeys Identified Mail) is a cryptographic signature. Your sending provider signs the headers and body with a private key, and publishes the matching public key in DNS at a selector you control. The receiver fetches the public key and verifies the signature. This proves two things SPF cannot: the message was authorized by someone holding your key, and the contents were not modified in transit. DKIM survives forwarding. SPF does not.
DMARC (Domain-based Message Authentication, Reporting and Conformance) is the policy layer. It tells receivers what to do when SPF and DKIM fail, and where to send reports about it. Crucially, DMARC also requires alignment: the domain in the visible From header has to match the domain that passed SPF or DKIM. Without DMARC, a message can pass SPF for some unrelated domain while displaying your brand in the From line. Alignment is what closes that hole.
You need all three. SPF alone is not enough, and it has been formally insufficient for major providers since Gmail and Yahoo tightened their sender requirements in 2024.
Here is a complete, working configuration for an app at yourapp.com sending transactional mail from the subdomain mail.yourapp.com.
; ---- SPF: exactly ONE record on the sending domain ----
mail.yourapp.com. TXT "v=spf1 include:_spf.yourprovider.com -all"
; ---- DKIM: public key published at the provider's selector ----
; The provider gives you the selector and the key. 2048-bit is the norm.
s1._domainkey.mail.yourapp.com. TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhki...IDAQAB"
; ---- DMARC: policy on the organizational domain ----
; Subdomains inherit unless sp= says otherwise.
_dmarc.yourapp.com. TXT "v=DMARC1; p=reject; rua=mailto:[email protected]; adkim=s; aspf=s; pct=100"
; ---- Optional but worth it: a return-path for bounce handling ----
bounces.mail.yourapp.com. CNAME feedback.yourprovider.com.
Four details in there are doing real work.
-all is a hard fail. The soft version, ~all, tells receivers to accept the message anyway and mark it suspicious. If you know your sending list, use the hard fail.
p=reject means receivers should throw away mail that fails authentication while claiming to be you. This is the whole point of DMARC and most domains never get there.
adkim=s and aspf=s set strict alignment. The From domain must match exactly, not just share an organizational parent. Relaxed alignment is the default and it is fine for most setups, but strict is worth it once you know your sending surface.
rua= is where aggregate reports go. Point it at an address you will read, or a service that parses the XML for you.
The four misconfigurations that break this in practice
Most broken setups fail in one of four specific ways, and all four are easy to check in under a minute.
Two SPF records. This is an automatic failure, not a partial one. RFC 7208 says a domain publishing more than one SPF record produces a permerror, and the whole check fails. It happens constantly: you add a provider, it hands you a TXT record, you paste it in alongside the one already there. The fix is to merge them into a single record with multiple include: mechanisms. While you are there, count your DNS lookups. SPF caps at ten, and each include: can chain into more.
DMARC parked at p=none forever. p=none means “tell me about failures and deliver the mail anyway.” It is the correct place to start, because it lets you find legitimate senders you forgot about before you start rejecting mail. It is not a destination. A domain sitting at p=none two years later gets reports nobody reads and no protection at all. Spend two weeks reading reports, fix what fails, move to p=quarantine, then to p=reject.
Sending from a subdomain the policy does not cover. DMARC on yourapp.com covers subdomains by inheritance, but SPF and DKIM do not inherit. If you send from mail.yourapp.com, that exact name needs its own SPF record and its own DKIM selector. A record on the parent does nothing for it.
Losing the record set in a migration. DNS lives with the domain, not the code. Changing providers, moving registrars, or switching off a hosted platform will all quietly drop authentication if nobody re-adds it.
Why transactional and marketing mail must not share a domain
Reputation at the major mailbox providers is tracked per sending domain, so a marketing campaign that collects complaints will drag your license delivery down with it. That is the entire argument and it is enough on its own.
Think about what the two streams look like to a spam filter. Your newsletter goes to thousands of addresses at once, some of which have not opened anything in a year, and a fraction hit “report spam” instead of unsubscribing. Your license email goes to one person actively waiting for it. Merging those profiles means the worse one sets the floor for both.
Split them at the subdomain level. Transactional on mail.yourapp.com, marketing on news.yourapp.com, each with its own SPF and DKIM. Most transactional providers support separate sending domains or subaccounts for exactly this.
The stronger version of this opinion: never send license email from your application server’s default mailer. A stock SMTP library pointed at a VPS gives you a cold IP with no reputation, no bounce handling, no complaint feedback loop, and no delivery telemetry. Use a dedicated transactional email provider. There are several good ones and the choice matters far less than the fact that you made it.
Content that gets a legitimate license email filtered
Authentication gets you past the front door. Content decides whether you land in the inbox or the promotions tab, and license emails have a specific problem: they structurally resemble phishing.
A bare license key with two lines of text around it looks exactly like a credential-harvesting message. Short body, urgent-feeling code, a link to click. Give the email context instead: what was purchased, which product and tier the key applies to, how many devices it covers, and how to reach a human. Long enough to read like a receipt from a real business.
The rest of the list, in rough order of how often it bites:
- All-image emails. A single graphic with the key rendered inside it. Filters cannot read it, screen readers cannot read it, and a low text-to-image ratio is a classic spam signal. It also means the customer cannot copy the key.
- Link shorteners. Never in transactional mail. Shorteners hide the destination, which is precisely why filters distrust them, and they add a redirect that can go down independently of you.
- Mismatched From and Reply-To. If the message says
[email protected]and replies route to[email protected], that inconsistency is scored against you. Keep both on the same authenticated domain. - No plain-text part. Always send
multipart/alternativewith a real text version, not an empty one. HTML-only messages are more likely to be filtered, and the text part is what shows up in previews and in clients with images off. It also survives every mail client ever written, which matters when the payload is a key the customer must copy exactly.
One more, specific to license keys: put the key in the body as selectable text, not only behind a button. Buttons are a fine addition, not a substitute, because the customer may be reading on a phone and typing on a laptop.
Bounces, complaints, and the resend path
Ignoring bounces degrades your domain reputation over time, and every hard bounce is also a paying customer sitting with no key. Both halves of that sentence need handling and most apps do neither.
A hard bounce means the address does not exist. Retrying it tells receivers you are not maintaining your list, which is a signal spam senders produce and legitimate ones do not. Suppress the address on the first hard bounce, permanently. Soft bounces are temporary, so retry with backoff and give up after a few days. Complaints, meaning someone marked your mail as spam, should suppress immediately and loudly, because a complaint on a transactional message usually means the customer did not recognize the sender.
// Transactional providers post delivery events to a webhook.
// Verify the signature, then split the outcome three ways.
export async function onEmailEvent(evt: EmailEvent) {
switch (evt.type) {
case 'bounce.hard':
await suppress(evt.to, 'hard_bounce')
// The important half: a customer paid and has no key.
await flagUnfulfilled({ orderId: evt.meta.orderId, reason: 'undeliverable' })
await notifyOps(`License email to ${evt.to} bounced. Order ${evt.meta.orderId}.`)
break
case 'bounce.soft':
// Transient. Provider usually retries; cap your own attempts.
await scheduleRetry(evt.meta.messageId, { maxAttempts: 3, backoff: '4h' })
break
case 'complaint':
await suppress(evt.to, 'complaint')
await notifyOps(`Spam complaint from ${evt.to}. Check the template.`)
break
}
}
The operational safety net is the part people skip. However good your deliverability is, some percentage of license emails will not arrive, and the answer to that cannot be a support ticket. The app itself, or a portal you link from your site, should let a customer enter their purchase email and get their key resent or displayed. That single path eliminates the most common support request in licensed software, which is covered in more depth in reducing license activation support tickets. A self-serve customer portal turns a failed email from a lost sale into a mild inconvenience.
Monitor delivery, do not wait for customers to tell you
You should find out about a deliverability problem from an alert, not from the second angry customer of the morning. This means treating email delivery as a monitored system rather than a fire-and-forget side effect.
Watch four numbers, all of which your provider exposes:
- Accepted delivery rate. Should be 99% or better on a transactional stream. Alert if a rolling hour drops below 97%.
- Hard bounce rate. Under 2%. A sudden spike usually means a form change let bad addresses through.
- Complaint rate. Under 0.1%. Gmail’s published threshold is 0.3% and you do not want to be near it.
- Time from payment to send. If your queue backs up, the email is technically delivered and practically useless. Ten minutes feels broken to someone who just paid.
Then correlate with the licensing side. The number that actually proves delivery worked is activation rate, because it measures whether a human received the key and used it. Email delivered but never activated is the pattern that tells you the message arrived and went unread, or landed in a tab nobody checks.
Wire the alert to whatever already pages you. The tool does not matter. Having no alert does.
You can do all of this yourself, and for a single product it is a reasonable afternoon of work. Add the DNS records, verify them with a free authentication checker, pick a transactional provider, write a template with real text alongside the HTML, wire the bounce webhook, put a resend page behind an email lookup. None of it is hard. It is a checklist nobody hands you, which is why it gets skipped.
The maintenance is what compounds. Templates drift, providers rotate DKIM keys, someone adds a second SPF record, a migration drops the DMARC policy, and the resend path needs to know which key belongs to which purchase and which devices are already bound to it. That last piece is where a licensing layer earns its place: Keylight sends the delivery email from an authenticated domain, tracks bounces against the order, and lets the customer retrieve or resend their key without touching your inbox. Stripe stays your payment processor. See pricing, or how license keys work for the layer underneath.
Either way, do the DNS today. It is thirty minutes, it is free, and it is the difference between a payment that becomes a customer and one that becomes a refund.
Frequently asked
Why is my license key email going to spam?+
The three usual causes are missing or broken authentication (SPF, DKIM, DMARC), sending from a domain that also sends marketing mail, and content that pattern-matches phishing, such as a bare code with almost no surrounding text. Fix authentication first, since it is the only one receivers check on every message.
Do I need SPF, DKIM, and DMARC for transactional email?+
Yes. Since 2024, Gmail and Yahoo require SPF or DKIM for all senders and full DMARC alignment for bulk senders. Transactional volume can spike past bulk thresholds during a launch, so treat all three as mandatory rather than optional.
Should license emails come from a subdomain?+
Yes. Send transactional mail from a dedicated subdomain such as mail.yourapp.com and keep marketing on a different one. Reputation is tracked per sending domain, so a subdomain isolates license delivery from a campaign that gets complaints.
What delivery rate should I expect for license emails?+
A properly authenticated transactional stream should sit at 99% or higher accepted, with hard bounces under 2%. If accepted delivery drops below 97% for a day, something changed in your DNS, your provider, or your content, and you should investigate before customers write in.
What should I do when a license email hard bounces?+
Suppress the address immediately so you never retry it, and surface the failure in your own dashboard as an unfulfilled order. A hard bounce means a paying customer has no key, so it needs a human follow-up, not just a suppression list entry.
Ready to ship?
Create your account and start licensing your apps in under a minute. Free forever tier included.
Start Free