<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lunea Labs</title>
    <description>The latest articles on DEV Community by Lunea Labs (@lunealabs).</description>
    <link>https://dev.to/lunealabs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4203983%2Fd93594b3-ae65-4ab9-9da2-10efb4da8515.png</url>
      <title>DEV Community: Lunea Labs</title>
      <link>https://dev.to/lunealabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lunealabs"/>
    <language>en</language>
    <item>
      <title>How to send license key emails from your SaaS</title>
      <dc:creator>Lunea Labs</dc:creator>
      <pubDate>Tue, 27 Oct 2026 19:56:56 +0000</pubDate>
      <link>https://dev.to/lunealabs/how-to-send-license-key-emails-from-your-saas-4o4e</link>
      <guid>https://dev.to/lunealabs/how-to-send-license-key-emails-from-your-saas-4o4e</guid>
      <description>&lt;p&gt;A license key email is the message your app sends right after a purchase, with the key the buyer needs to activate your software. Send it from your server when your payment provider confirms the payment, not from the checkout page. Put the key in the body (never the subject line), in a format that is easy to copy. Add the product, the number of seats, the download links and a way to find the key again. Send it from your own authenticated domain, record that you sent it so it never goes out twice, and keep promotions out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a license key email must contain
&lt;/h2&gt;

&lt;p&gt;Buyers' "where is my key?" threads show what goes wrong: keys land in spam, addresses are mistyped, keys get lost. A good email prevents those questions. Include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The key itself&lt;/strong&gt;, on its own line, in a monospace font, with nothing else on that line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it covers&lt;/strong&gt;: product name, plan, number of seats, and the expiry or renewal date if there is one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to activate it&lt;/strong&gt;: two or three short steps, or one link to your activation docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download links&lt;/strong&gt;, if the product is installed software.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where to find the key later&lt;/strong&gt;: a link to the account or billing page where the key is also shown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who to contact&lt;/strong&gt;: a reply-to address that a person reads, not &lt;code&gt;no-reply@&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An order reference&lt;/strong&gt;, so support can find the purchase quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sketch's keys start with &lt;code&gt;SK3&lt;/code&gt;, so buyers can &lt;a href="https://www.sketch.com/support/mac-only-licenses/license-key/" rel="noopener noreferrer"&gt;search their inbox for it&lt;/a&gt;. A fixed prefix on your keys saves support tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to send it: after the payment is confirmed
&lt;/h2&gt;

&lt;p&gt;Send the email from your backend when your payment provider confirms the payment. With Stripe Checkout, that is the &lt;code&gt;checkout.session.completed&lt;/code&gt; webhook (a webhook is an HTTP request the provider sends to your server when something happens), plus &lt;code&gt;checkout.session.async_payment_succeeded&lt;/code&gt; for slower payment methods such as bank transfers.&lt;/p&gt;

&lt;p&gt;Two rules from &lt;a href="https://docs.stripe.com/checkout/fulfillment" rel="noopener noreferrer"&gt;Stripe's fulfilment guide&lt;/a&gt; apply to every provider:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't rely on the thank-you page.&lt;/strong&gt; A buyer can pay and lose their connection before it loads. The webhook goes to your server directly and is retried if delivery fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect to be called more than once.&lt;/strong&gt; Stripe says your fulfilment function "might be called multiple times, possibly concurrently, for the same Checkout Session." Send on every call and some buyers get two keys.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So: confirm the payment, assign the key, save it with the order, then send the email and record the send. If the email fails, retry it without creating a new key.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;From the LuneaMail team:&lt;/strong&gt; We built LuneaMail because we needed to send highly customised emails from our own product, and there was no simple way to do it. With Gmail we ended up rendering templates in Google Sheets and trying plugins, and the result was far from what we wanted. HubSpot took many steps to send one styled email, and its templates limited what we could build. So we built the tool we wanted as developers: paste your HTML, make it dynamic with Handlebars, send your data as JSON, and the email goes out looking exactly as designed. A license key email is that idea in its simplest form: one person, one moment, their own data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A license key email template you can copy
&lt;/h2&gt;

&lt;p&gt;Replace the parts in &lt;code&gt;{{double braces}}&lt;/code&gt; with your data. The same names are used in the API example below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subject:&lt;/strong&gt; Your {{product}} license key&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preheader&lt;/strong&gt; (the grey preview text after the subject in most inboxes): Your key, {{seats}} seats, and how to activate it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Body:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hi {{firstName}},&lt;/p&gt;

&lt;p&gt;Thanks for buying {{product}}. Here is your license key:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{{licenseKey}}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It includes {{seats}} seats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To activate:&lt;/strong&gt; open {{product}}, go to Settings › License, and paste the key. &lt;em&gt;(Use your product's real steps.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Download:&lt;/strong&gt; macOS · Windows&lt;/p&gt;

&lt;p&gt;You can see this key at any time in your account: {{accountUrl}}&lt;/p&gt;

&lt;p&gt;Order reference: {{orderId}}&lt;/p&gt;

&lt;p&gt;Questions? Reply to this email and a person will answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keep the plain-text version the same. For several keys in one order, repeat a block with &lt;code&gt;{{#each}}&lt;/code&gt;, as the download links do below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send it with one API call
&lt;/h2&gt;

&lt;p&gt;With &lt;a href="https://luneamail.com/transactional-email" rel="noopener noreferrer"&gt;LuneaMail's transactional API&lt;/a&gt;, the template lives in your workspace, designed in the visual builder or written in HTML, and your server sends only the data. Variables use Handlebars: &lt;code&gt;{{licenseKey}}&lt;/code&gt; inserts a value, &lt;code&gt;{{#if seats}}…{{/if}}&lt;/code&gt; shows a section only when the data has it, and &lt;code&gt;{{#each downloads}}…{{/each}}&lt;/code&gt; repeats a block for each item in a list. In the visual builder, a section's "Show this section" condition becomes the same &lt;code&gt;{{#if}}&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;The template body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Hi {{firstName}}, thanks for buying {{product}}.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Your license key: &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;{{licenseKey}}&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

{{#if seats}}
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;It includes {{seats}} team seats.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
{{/if}}

{{#each downloads}}
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{url}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Download for {{name}}&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
{{/each}}

&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Find this key any time in &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{accountUrl}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;your account&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Order reference: {{orderId}}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call from your fulfilment code (Node.js):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendLicenseEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.luneamail.com/api/v1/send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LUNEAMAIL_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;templateId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LICENSE_TEMPLATE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your {{product}} license key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;licenseKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;licenseKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accountUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/account/licenses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;downloads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;macOS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/download/mac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Windows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/download/windows&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;messageId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// { status: "queued", messageId }&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markLicenseEmailSent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`License email not sent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subject takes the same variables as the body. &lt;code&gt;to&lt;/code&gt; is one address per call.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the API can answer
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;202&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;Accepted and queued&lt;/td&gt;
&lt;td&gt;Save the &lt;code&gt;messageId&lt;/code&gt; with the order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MISSING_VARIABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The template uses a variable you didn't send&lt;/td&gt;
&lt;td&gt;Fix the data; nothing was sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INVALID_TEMPLATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wrong template ID, or the template is empty&lt;/td&gt;
&lt;td&gt;Check the ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UNAUTHORIZED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing or wrong API key&lt;/td&gt;
&lt;td&gt;Check the key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;422&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SUPPRESSED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The address bounced or reported spam before&lt;/td&gt;
&lt;td&gt;Don't retry; show the key in the app and ask for another address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RATE_LIMITED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;More than 600 sends a minute on this key&lt;/td&gt;
&lt;td&gt;Wait for the &lt;code&gt;Retry-After&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;429&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QUOTA_EXCEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your monthly email allowance is used up&lt;/td&gt;
&lt;td&gt;Top up or upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;502&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SEND_FAILED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The send failed downstream&lt;/td&gt;
&lt;td&gt;Retry later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;MISSING_VARIABLE&lt;/code&gt; is the useful one. Variables outside &lt;code&gt;{{#if}}&lt;/code&gt; and &lt;code&gt;{{#each}}&lt;/code&gt; are required, so if your code forgets &lt;code&gt;licenseKey&lt;/code&gt;, the API refuses instead of sending "Your license key:" with nothing after it. More on the &lt;a href="https://luneamail.com/developers" rel="noopener noreferrer"&gt;developers page&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't send it twice
&lt;/h3&gt;

&lt;p&gt;With two webhook deliveries at the same moment, "check if sent, then send" can pass twice. Claim the order first with one atomic update, for example &lt;code&gt;UPDATE orders SET license_email = 'sending' WHERE id = $1 AND license_email IS NULL&lt;/code&gt;, and only send if a row changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format the key so people can copy it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monospace, on its own line,&lt;/strong&gt; with no full stop after it to copy by mistake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groups with dashes&lt;/strong&gt;, like &lt;code&gt;ABCD-1234-EFGH&lt;/code&gt;, so it can be read aloud to support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No look-alike characters&lt;/strong&gt; in your key alphabet: &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;O&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;I&lt;/code&gt; and &lt;code&gt;l&lt;/code&gt;. Someone will type it from a screenshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept the key with or without dashes and spaces&lt;/strong&gt; when activating. Pasted text often has a trailing space.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security: what not to put in the email
&lt;/h2&gt;

&lt;p&gt;A license key is a credential: whoever has it can use what the buyer paid for. Email is the expected way to deliver it, so limit the damage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not in the subject line.&lt;/strong&gt; Subjects appear in phone notifications, on lock screens and in inbox lists. "Your Pro license key" is fine; the key itself is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the key in the account too,&lt;/strong&gt; and let the owner replace it. A leaked email then has a fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For high-value keys, send a link instead.&lt;/strong&gt; The email says "Your key is ready" and links to the account page behind a login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't log the request body.&lt;/strong&gt; If your app logs every outgoing API call, your logs now hold every key you've sold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resend only to the address on the order.&lt;/strong&gt; A "send my key again" form that accepts any address lets anyone collect keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Make sure it arrives
&lt;/h2&gt;

&lt;p&gt;A key in the spam folder becomes a support ticket, or a refund request.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Send from your own domain, authenticated.&lt;/strong&gt; Gmail &lt;a href="https://support.google.com/a/answer/81126" rel="noopener noreferrer"&gt;requires SPF or DKIM from every sender&lt;/a&gt; (SPF and DKIM are DNS records that prove a mail server may send for your domain). In LuneaMail you add your domain and its DKIM records once. Until the domain is verified, mail goes out from LuneaMail's address with replies going to yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a reply-to someone reads.&lt;/strong&gt; Buyers reply to license emails with real problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle bounces.&lt;/strong&gt; If an address bounced before, the API returns &lt;code&gt;SUPPRESSED&lt;/code&gt; and sends nothing. Show the key in the account too, so a bad address never blocks a buyer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Transactional, not marketing
&lt;/h2&gt;

&lt;p&gt;A license key email is transactional: it delivers what someone just paid for. The &lt;a href="https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business" rel="noopener noreferrer"&gt;FTC's CAN-SPAM guide&lt;/a&gt; counts messages that "deliver goods or services as part of a transaction that the recipient already has agreed to" as transactional, so you need no marketing consent and no unsubscribe link. In LuneaMail, people who unsubscribed from your newsletter still get their license emails.&lt;/p&gt;

&lt;p&gt;The catch: the FTC judges mixed emails by their primary purpose. A subject that reads like an ad, or a promotion above the key, can make the whole email commercial. So no discount banner, no newsletter sign-up, no "you might also like". Upsell in a separate marketing email, to people who agreed to receive one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resends, wrong addresses and refunds
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resend:&lt;/strong&gt; add an "Email my key again" button in the account. It calls the same function with the same template and the same key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong address:&lt;/strong&gt; buyers mistype emails. Let support change the address on an order, then resend. The key should never be emailed to the new address without that check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund or chargeback:&lt;/strong&gt; revoke the key on your license server. A short, factual notice is transactional too.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Email sent from the payment webhook, not the thank-you page&lt;/li&gt;
&lt;li&gt;[ ] One email per order, even if the webhook arrives twice&lt;/li&gt;
&lt;li&gt;[ ] Key in the body, monospace, on its own line; not in the subject&lt;/li&gt;
&lt;li&gt;[ ] Product, plan, seats, activation steps and order reference&lt;/li&gt;
&lt;li&gt;[ ] Link to the account page where the key is also shown&lt;/li&gt;
&lt;li&gt;[ ] A reply-to address a person reads&lt;/li&gt;
&lt;li&gt;[ ] Sent from your own domain with SPF and DKIM&lt;/li&gt;
&lt;li&gt;[ ] No promotions anywhere in the email&lt;/li&gt;
&lt;li&gt;[ ] Resend button that only sends to the order's address&lt;/li&gt;
&lt;li&gt;[ ] Key revoked on refund&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sending from the checkout success page.&lt;/strong&gt; Some buyers never reach it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No idempotency.&lt;/strong&gt; Duplicate webhooks mean duplicate keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The key in the subject line.&lt;/strong&gt; Visible on every lock screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A blank key.&lt;/strong&gt; A missing variable rendered as an empty string. Fail loudly instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An upsell above the key.&lt;/strong&gt; It can turn a transactional email into a marketing one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The key only in email.&lt;/strong&gt; Lose the email and the buyer has to contact support.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sending license keys with LuneaMail
&lt;/h2&gt;

&lt;p&gt;LuneaMail's transactional API is one &lt;code&gt;POST /send&lt;/code&gt; call with a template ID, an address and your data. Your team designs the template in the same editor as your newsletters, so license emails match your brand without a second tool, and they use the same monthly allowance as campaigns on every plan, including Free (&lt;a href="https://luneamail.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;). The same pattern works for other app email; see our guide to &lt;a href="https://luneamail.com/blog/team-invite-emails" rel="noopener noreferrer"&gt;team invite emails&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://luneamail.com/blog/license-key-emails" rel="noopener noreferrer"&gt;LuneaMail blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>saas</category>
      <category>startup</category>
    </item>
    <item>
      <title>Gmail and Yahoo bulk sender requirements for your app emails</title>
      <dc:creator>Lunea Labs</dc:creator>
      <pubDate>Mon, 26 Oct 2026 20:23:11 +0000</pubDate>
      <link>https://dev.to/lunealabs/gmail-and-yahoo-bulk-sender-requirements-for-your-app-emails-2pgj</link>
      <guid>https://dev.to/lunealabs/gmail-and-yahoo-bulk-sender-requirements-for-your-app-emails-2pgj</guid>
      <description>&lt;p&gt;Gmail and Yahoo have applied stricter rules to bulk senders since February 2024. Google calls you a bulk sender if you send close to 5,000 or more messages to personal Gmail accounts in 24 hours. Bulk senders must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authenticate with &lt;strong&gt;SPF and DKIM&lt;/strong&gt;, and publish a &lt;strong&gt;DMARC&lt;/strong&gt; record (&lt;code&gt;p=none&lt;/code&gt; is the minimum),&lt;/li&gt;
&lt;li&gt;make sure the visible From domain &lt;strong&gt;aligns&lt;/strong&gt; with the SPF or DKIM domain,&lt;/li&gt;
&lt;li&gt;add &lt;strong&gt;one-click unsubscribe&lt;/strong&gt; to marketing emails and honor opt-outs within two days,&lt;/li&gt;
&lt;li&gt;keep the &lt;strong&gt;spam complaint rate below 0.3%&lt;/strong&gt;, and aim for under 0.1%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Smaller senders still need SPF or DKIM, valid reverse DNS, TLS and a spam rate below 0.3%.&lt;/p&gt;

&lt;p&gt;This article explains each rule as Google and Yahoo state it today, with links. It also covers what the rules mean for app emails like password resets and receipts, and a few launch problems we hit at LuneaMail that no rule mentions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who counts as a bulk sender
&lt;/h2&gt;

&lt;p&gt;Google's &lt;a href="https://support.google.com/a/answer/14229414" rel="noopener noreferrer"&gt;sender guidelines FAQ&lt;/a&gt; defines a bulk sender as one that "sends close to 5,000 messages or more to personal Gmail accounts" within 24 hours. Two details in that FAQ matter for startups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Messages sent from the same primary domain count toward the 5,000 limit."&lt;/strong&gt; Your app emails, newsletter and product updates all add up if they share &lt;code&gt;example.com&lt;/code&gt;, even when they use different subdomains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Senders who meet the above criteria at least once are permanently considered bulk senders."&lt;/strong&gt; One busy launch day is enough. You don't go back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Yahoo's &lt;a href="https://senders.yahooinc.com/best-practices/" rel="noopener noreferrer"&gt;Sender Hub&lt;/a&gt; does not publish a volume number. Treat the bulk rules as your baseline from the first email. They are not hard to meet, and an early-stage SaaS can cross the line in a single launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules side by side
&lt;/h2&gt;

&lt;p&gt;These are the requirements as Google's &lt;a href="https://support.google.com/a/answer/81126" rel="noopener noreferrer"&gt;email sender guidelines&lt;/a&gt; and Yahoo's &lt;a href="https://senders.yahooinc.com/best-practices/" rel="noopener noreferrer"&gt;best practices&lt;/a&gt; state them on 24 October 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;All senders&lt;/th&gt;
&lt;th&gt;Bulk senders&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;SPF &lt;strong&gt;or&lt;/strong&gt; DKIM&lt;/td&gt;
&lt;td&gt;SPF &lt;strong&gt;and&lt;/strong&gt; DKIM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DMARC&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;td&gt;Required; policy can be &lt;code&gt;p=none&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;From alignment&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;td&gt;From domain aligned with the SPF or DKIM domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forward and reverse DNS (PTR)&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS connection&lt;/td&gt;
&lt;td&gt;Required (Google)&lt;/td&gt;
&lt;td&gt;Required (Google)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message format&lt;/td&gt;
&lt;td&gt;RFC 5322 (Yahoo also lists RFC 5321)&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spam rate&lt;/td&gt;
&lt;td&gt;Below 0.3%&lt;/td&gt;
&lt;td&gt;Below 0.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-click unsubscribe&lt;/td&gt;
&lt;td&gt;Not required&lt;/td&gt;
&lt;td&gt;Marketing and subscribed messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honor unsubscribes&lt;/td&gt;
&lt;td&gt;Not stated&lt;/td&gt;
&lt;td&gt;Within 2 days (Yahoo), 48 hours (Google)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few terms. &lt;strong&gt;PTR&lt;/strong&gt; (reverse DNS) maps the sending server's IP address back to a name. Your email provider handles PTR and TLS for you, so as a SaaS team you mostly own the other rows. &lt;strong&gt;Alignment&lt;/strong&gt; means the domain people see in the From address matches the domain that passed SPF or DKIM. Yahoo says "relaxed alignment is acceptable", so &lt;code&gt;mail.example.com&lt;/code&gt; can align with &lt;code&gt;example.com&lt;/code&gt;. Google judges it at the organizational-domain level and only needs SPF or DKIM to align, not both.&lt;/p&gt;

&lt;p&gt;Google adds one more DKIM detail: sending to personal Gmail accounts "requires a DKIM key of 1024 bits or longer". Any current ESP meets this.&lt;/p&gt;

&lt;p&gt;If you haven't set up the records yet, our &lt;a href="https://luneamail.com/blog/spf-dkim-dmarc-setup" rel="noopener noreferrer"&gt;SPF, DKIM and DMARC setup guide&lt;/a&gt; walks through it with example DNS records.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the rules mean for app emails
&lt;/h2&gt;

&lt;p&gt;Your SaaS sends two kinds of email:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transactional&lt;/strong&gt;: password resets, verification links, receipts, license keys, security alerts. One person triggered them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing or subscribed&lt;/strong&gt;: newsletters, product announcements, onboarding drips, promotions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The authentication rules, the alignment rule and the spam-rate limit apply to &lt;strong&gt;both&lt;/strong&gt;. The volume counts both too.&lt;/p&gt;

&lt;p&gt;The unsubscribe rule is different. Google's FAQ says: "One-click unsubscribe is required only for marketing and promotional messages." It gives password resets and reservation confirmations as examples of transactional messages that don't need it. So your password reset email doesn't need an unsubscribe header. Your weekly product update does.&lt;/p&gt;

&lt;p&gt;Be honest about the line. A "receipt" that is half promotion is a marketing email. If you're unsure, add the header. It costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  One-click unsubscribe (RFC 8058)
&lt;/h2&gt;

&lt;p&gt;One-click unsubscribe means the mail app shows an "Unsubscribe" button next to your sender name. One click, no login, no "are you sure" page. It works through two email headers defined in &lt;a href="https://www.rfc-editor.org/rfc/rfc8058" rel="noopener noreferrer"&gt;RFC 8058&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;List-Unsubscribe: &amp;lt;https://example.com/unsubscribe/one-click/abc123&amp;gt;
List-Unsubscribe-Post: List-Unsubscribe=One-Click
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL must be &lt;code&gt;https&lt;/code&gt;. When the person clicks, their mail provider sends a &lt;code&gt;POST&lt;/code&gt; request to it, with no cookies or login. Your server must unsubscribe them from that request alone. RFC 8058 says the URL should contain a hard-to-forge identifier, so use a unique token per recipient, not their email address in plain text.&lt;/p&gt;

&lt;p&gt;Google also requires "a clearly visible unsubscribe link in the message body". The header and the link work together. Yahoo calls the RFC 8058 POST method "highly recommended" and asks you to honor unsubscribes within 2 days. Google recommends 48 hours.&lt;/p&gt;

&lt;p&gt;According to Google's FAQ, a missing one-click header does not cause automatic rejection. But people who can't unsubscribe easily press "Report spam" instead, and that hurts the next rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spam rate: aim for 0.1%, never reach 0.3%
&lt;/h2&gt;

&lt;p&gt;The spam rate is the share of your emails that recipients mark as spam. Google says to "keep spam rates reported in Postmaster Tools below 0.10% and avoid ever reaching a spam rate of 0.30% or higher". That is 1 complaint per 1,000 emails as a target, and 3 per 1,000 as the hard limit. Google calculates it daily. Yahoo calculates its rate "based on mail delivered to the inbox".&lt;/p&gt;

&lt;p&gt;To watch your Gmail numbers, add your domain to &lt;a href="https://postmaster.google.com/" rel="noopener noreferrer"&gt;Google Postmaster Tools&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What keeps the rate low:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email only people who signed up, and send what they signed up for.&lt;/li&gt;
&lt;li&gt;Make unsubscribing easier than reporting spam.&lt;/li&gt;
&lt;li&gt;Stop emailing addresses that bounce or complain. Automatically, forever.&lt;/li&gt;
&lt;li&gt;Don't judge engagement by open rates alone (see below).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Opens can lie
&lt;/h3&gt;

&lt;p&gt;Apple Mail Privacy Protection loads every email automatically, including the tracking pixel. Those "opens" are not people reading. If you count them, inactive contacts look engaged and you keep emailing them, which pushes up complaints. When we built LuneaMail's analytics, we stored those opens separately as machine opens. We also stopped counting clicks on unsubscribe and other footer links as engagement. A click on "Unsubscribe" is not interest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens if you don't comply
&lt;/h2&gt;

&lt;p&gt;Google's FAQ says: "Starting November 2025, Gmail is ramping up its enforcement on non-compliant traffic." Enforcement is "gradual and progressive", and faster for new domains. In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temporary errors (&lt;code&gt;4.7.x&lt;/code&gt;)&lt;/strong&gt;: Gmail delays the message. The error code tells you which requirement failed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permanent errors (&lt;code&gt;5.7.x&lt;/code&gt;)&lt;/strong&gt;: the message is blocked.&lt;/li&gt;
&lt;li&gt;Messages may also go straight to spam.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your spam rate spikes, Google only offers mitigation once it stays below 0.3% for 7 consecutive days. Microsoft announced &lt;a href="https://techcommunity.microsoft.com/blog/outlook/strengthening-email-ecosystem-outlook%E2%80%99s-new-requirements-for-high%E2%80%90volume-senders/4399730" rel="noopener noreferrer"&gt;its own requirements for high-volume senders to Outlook&lt;/a&gt; in 2025. Read that post too if many of your users have Outlook.com addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The launch problem no rule mentions: your provider's plan
&lt;/h2&gt;

&lt;p&gt;You can pass every rule above and still send nothing. This happened to us during LuneaMail's launch.&lt;/p&gt;

&lt;p&gt;We sent through SendGrid. The account was on a free trial. The trial ended without us noticing, and SendGrid started refusing mail with a "Maximum credits exceeded" error. Every email stopped, including the ones that matter most: password resets and sign-up verification links. That means a new user can't confirm their account, and a locked-out user can't get back in.&lt;/p&gt;

&lt;p&gt;Before launch, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Your email provider's plan, its monthly and daily limits, and when any trial ends&lt;/li&gt;
&lt;li&gt;[ ] What happens at the limit: a hard stop or overage billing&lt;/li&gt;
&lt;li&gt;[ ] An alert when sending fails, not just when the provider emails you&lt;/li&gt;
&lt;li&gt;[ ] That a password reset and a sign-up email arrive in a real Gmail and Yahoo inbox&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;From the LuneaMail team:&lt;/strong&gt; When you build your own email sending, you also take on the job of making sure it actually gets delivered. Authentication, unsubscribes, spam rates and your provider's limits all become your problem, and you usually find a gap only when someone doesn't get an email they were waiting for. That's a huge undertaking for a team that just wants to send a few branded emails, and a large part of why we built LuneaMail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sending from &lt;code&gt;@gmail.com&lt;/code&gt; through an ESP.&lt;/strong&gt; Google's guidelines say "Don't impersonate Gmail From: headers" and point to Gmail's DMARC quarantine policy. Use an address on your own domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsubscribe link with no header.&lt;/strong&gt; The body link alone doesn't give the one-click button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An unsubscribe page that needs a login.&lt;/strong&gt; It is not one click, and people report spam instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating onboarding drips as transactional.&lt;/strong&gt; If it promotes features or upgrades, it's marketing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not suppressing bounces and complaints.&lt;/strong&gt; Every repeat send to a complainer adds to your spam rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting open rates.&lt;/strong&gt; Apple's automatic opens make dead lists look alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How LuneaMail handles this
&lt;/h2&gt;

&lt;p&gt;We built these rules into LuneaMail's send path, so a campaign can't go out without them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every campaign and automation email carries &lt;code&gt;List-Unsubscribe&lt;/code&gt; and &lt;code&gt;List-Unsubscribe-Post: List-Unsubscribe=One-Click&lt;/code&gt; headers, plus a visible unsubscribe link in the footer.&lt;/li&gt;
&lt;li&gt;The footer includes your postal address. LuneaMail won't send a campaign until you add one in &lt;strong&gt;Settings › Brand kit&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;From addresses at free mailbox providers like Gmail, Yahoo or Outlook are refused for campaigns. You send from your own domain, which you can authenticate in &lt;strong&gt;Settings › Domains&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The workspace owner must confirm their email address before sending to a list.&lt;/li&gt;
&lt;li&gt;Unsubscribes, bounces and spam complaints are suppressed automatically.&lt;/li&gt;
&lt;li&gt;Apple's automatic opens are counted as machine opens, not real opens, and footer-link clicks don't count as engagement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;App emails like receipts and license keys go through the same authenticated domain with the &lt;a href="https://luneamail.com/transactional-email" rel="noopener noreferrer"&gt;transactional email API&lt;/a&gt;. See the &lt;a href="https://luneamail.com/features" rel="noopener noreferrer"&gt;features page&lt;/a&gt; and the &lt;a href="https://luneamail.com/developers" rel="noopener noreferrer"&gt;developer docs&lt;/a&gt; for details.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://luneamail.com/blog/gmail-yahoo-bulk-sender-rules" rel="noopener noreferrer"&gt;LuneaMail blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
