<?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: Hani Khanani</title>
    <description>The latest articles on DEV Community by Hani Khanani (@hani_khanani_2842a21e1b57).</description>
    <link>https://dev.to/hani_khanani_2842a21e1b57</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%2F4213258%2F07310dea-f8da-447a-ad29-dc2b913eb738.jpg</url>
      <title>DEV Community: Hani Khanani</title>
      <link>https://dev.to/hani_khanani_2842a21e1b57</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hani_khanani_2842a21e1b57"/>
    <language>en</language>
    <item>
      <title>How to send Listmonk newsletters through OwnMailServer</title>
      <dc:creator>Hani Khanani</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:59:55 +0000</pubDate>
      <link>https://dev.to/hani_khanani_2842a21e1b57/how-to-send-listmonk-newsletters-through-ownmailserver-3fej</link>
      <guid>https://dev.to/hani_khanani_2842a21e1b57/how-to-send-listmonk-newsletters-through-ownmailserver-3fej</guid>
      <description>&lt;p&gt;To send newsletters and campaigns with OwnMailServer, run &lt;a href="https://listmonk.app" rel="noopener noreferrer"&gt;Listmonk&lt;/a&gt; for your contacts, lists and campaigns, and point its SMTP settings at us: &lt;code&gt;smtp.ownmailserver.com&lt;/code&gt;, port 587, STARTTLS, with an API key as the password. Then slow Listmonk down to our 10 messages a second, cap it at your daily warm-up limit, and forward our bounce webhooks to Listmonk. It takes about an hour.&lt;/p&gt;

&lt;p&gt;OwnMailServer is a sending service: it delivers email, handles bounces and complaints, warms up your sending and watches your reputation. It doesn't store contact lists or edit campaigns. Listmonk does, and it's free, open source and self-hosted, so your subscriber list stays in your own database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An approved OwnMailServer account with a &lt;strong&gt;verified sending domain&lt;/strong&gt;. Listmonk's From address must be on that domain, or we refuse the email with &lt;code&gt;550&lt;/code&gt;. The &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; list the DNS records.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;API key&lt;/strong&gt;, created in your dashboard under &lt;strong&gt;API keys &amp;amp; SMTP&lt;/strong&gt;. Choose &lt;strong&gt;Sending only&lt;/strong&gt; (the default), limited to your sending domain. It's the SMTP password.&lt;/li&gt;
&lt;li&gt;A server with &lt;strong&gt;Docker&lt;/strong&gt; to run Listmonk. A small VPS is enough for most lists.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;domain name for Listmonk&lt;/strong&gt; with HTTPS, such as &lt;code&gt;lists.example.com&lt;/code&gt;. Unsubscribe links, the subscription form and the archive all point to it, so readers must be able to reach it.&lt;/li&gt;
&lt;li&gt;A list you can stand behind: people who signed up and expect your email. We review every account's list source, and bought or scraped lists get accounts paused quickly through bounces and complaints.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Install Listmonk with Docker
&lt;/h2&gt;

&lt;p&gt;Listmonk's official Docker Compose file starts Listmonk and its PostgreSQL database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;listmonk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;listmonk
curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://github.com/knadh/listmonk/raw/master/docker-compose.yml
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listmonk listens on port 9000. Open &lt;code&gt;http://&amp;lt;your-server&amp;gt;:9000&lt;/code&gt; and create the Super Admin user. As of October 2026, the current stable release is v6.2.0, with v7 in beta. The compose file uses the stable image.&lt;/p&gt;

&lt;p&gt;Put Listmonk behind a reverse proxy with HTTPS (nginx, Caddy or Traefik) on &lt;code&gt;lists.example.com&lt;/code&gt; before you send anything. Then, in &lt;strong&gt;Settings → General&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root URL&lt;/strong&gt;: &lt;code&gt;https://lists.example.com&lt;/code&gt;. Listmonk builds unsubscribe and tracking links from it. Leaving it on &lt;code&gt;localhost&lt;/code&gt; breaks every unsubscribe link you send.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;'From' emails&lt;/strong&gt;: an address on your verified domain, such as &lt;code&gt;Example News &amp;lt;news@example.com&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: Connect Listmonk to OwnMailServer over SMTP
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Settings → SMTP&lt;/strong&gt;, edit the first server (or add one) and enter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Listmonk field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;smtp.ownmailserver.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;587&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth protocol&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LOGIN&lt;/code&gt; (&lt;code&gt;PLAIN&lt;/code&gt; also works)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ownmailserver&lt;/code&gt; (we accept any value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;Your API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;STARTTLS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skip TLS verification&lt;/td&gt;
&lt;td&gt;Off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max. connections&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retries&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Save, then use &lt;strong&gt;Send e-mail&lt;/strong&gt; on the SMTP settings page to send yourself a test. If the test fails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;535&lt;/code&gt;: the password isn't a valid API key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;550&lt;/code&gt;: the From address isn't on a verified domain, or the account is still pending approval.&lt;/li&gt;
&lt;li&gt;Connection timeout: your server's firewall or hosting provider is blocking outbound port 587.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;SMTP section of our API docs&lt;/a&gt; lists every reply code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Match Listmonk's speed to our limits
&lt;/h2&gt;

&lt;p&gt;Listmonk's defaults are built to send as fast as the mail server allows: 10 workers, each sending up to 10 messages a second. That's up to 100 a second. We accept 10 a second per account and answer the rest with a temporary &lt;code&gt;450&lt;/code&gt;. Listmonk counts those as errors and pauses the campaign once there are too many.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Settings → Performance&lt;/strong&gt;, set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Two workers sending at once.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message rate&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 × 4 = 8 messages a second, under our 10.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch size&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fine as is; it only controls how many subscribers Listmonk reads from its database at a time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum error threshold&lt;/td&gt;
&lt;td&gt;Leave the default&lt;/td&gt;
&lt;td&gt;A paused campaign is a useful signal that something is wrong.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 8 a second, Listmonk sends about 28,000 emails an hour. If you need more, ask us to raise your account's rate, then raise Message rate to match.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stay under your warm-up cap
&lt;/h3&gt;

&lt;p&gt;A new account can only reach a limited number of recipients in any 24 hours. The cap starts at a few hundred and grows about 25% after each clean day. Your dashboard shows the current number, and our &lt;a href="https://ownmailserver.com/guides/email-ip-warm-up" rel="noopener noreferrer"&gt;warm-up guide&lt;/a&gt; explains how it grows. Past the cap, we answer with &lt;code&gt;450&lt;/code&gt; until the window clears.&lt;/p&gt;

&lt;p&gt;Listmonk can hold to that number itself. In &lt;strong&gt;Settings → Performance&lt;/strong&gt;, turn on &lt;strong&gt;Enable sliding window limit&lt;/strong&gt; and set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duration&lt;/strong&gt;: &lt;code&gt;24h&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max. messages&lt;/strong&gt;: your current daily cap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Raise &lt;strong&gt;Max. messages&lt;/strong&gt; as your cap grows. A campaign bigger than the cap then goes out over several days instead of stopping halfway. Transactional mail sent through the same account counts toward the same cap, so leave some room for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Keep one-click unsubscribe working
&lt;/h2&gt;

&lt;p&gt;Gmail and Yahoo require bulk senders to offer one-click unsubscribe (RFC 8058) and to honour it within two days. See our guide to the &lt;a href="https://ownmailserver.com/guides/gmail-yahoo-bulk-sender-requirements" rel="noopener noreferrer"&gt;Gmail and Yahoo sender requirements&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Listmonk handles this for you. In &lt;strong&gt;Settings → Privacy&lt;/strong&gt;, keep &lt;strong&gt;Include &lt;code&gt;List-Unsubscribe&lt;/code&gt; header&lt;/strong&gt; on. Listmonk then adds &lt;code&gt;List-Unsubscribe&lt;/code&gt; and &lt;code&gt;List-Unsubscribe-Post&lt;/code&gt; headers that point at your Listmonk server, and records the unsubscribe on the subscriber when a reader clicks.&lt;/p&gt;

&lt;p&gt;Here's how that works with OwnMailServer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When an email already has its own &lt;code&gt;List-Unsubscribe&lt;/code&gt; header, we don't add ours, so readers see exactly one Unsubscribe button.&lt;/li&gt;
&lt;li&gt;We treat the email as marketing. If someone unsubscribed through one of our own links in the past, we won't send them Listmonk campaigns, but they still get your receipts and password resets.&lt;/li&gt;
&lt;li&gt;Our DKIM signature covers both unsubscribe headers. Gmail and Yahoo only show the button when it does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep your Root URL on public HTTPS. A one-click unsubscribe is a POST from Gmail's servers to that URL, and it fails if Listmonk can't be reached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Send bounces and complaints back to Listmonk
&lt;/h2&gt;

&lt;p&gt;We already protect your sending: hard bounces and spam complaints go on your suppression list automatically. If Listmonk sends to a suppressed address, we drop that email. It isn't sent, isn't charged and doesn't count toward your cap. Your reputation is safe even if Listmonk never hears about the bounce.&lt;/p&gt;

&lt;p&gt;Your Listmonk lists still go stale, though, and your campaign numbers include addresses that will never receive anything. To keep them clean, pass our webhook events to Listmonk's bounce endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In Listmonk:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In &lt;strong&gt;Settings → Bounces&lt;/strong&gt;, turn on &lt;strong&gt;Enable bounce processing&lt;/strong&gt; and &lt;strong&gt;Enable bounce webhooks&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the bounce actions. A sensible setup: Hard, count 1, Blocklist. Complaint, count 1, Blocklist. Soft, count 2, None.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Users&lt;/strong&gt;, create a user of type &lt;strong&gt;API&lt;/strong&gt; whose role can post bounces, and copy its token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;In OwnMailServer:&lt;/strong&gt; add a webhook in your dashboard for &lt;code&gt;email.bounced&lt;/code&gt; and &lt;code&gt;email.complained&lt;/code&gt;, pointing at the small relay below. Copy the signing secret it shows you once.&lt;/p&gt;

&lt;p&gt;The relay checks our signature, then posts each event to Listmonk. It uses only Node.js built-ins (Node 18 or later):&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="c1"&gt;// relay.mjs: OwnMailServer webhooks -&amp;gt; Listmonk bounces&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timingSafeEqual&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&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;SECRET&lt;/span&gt; &lt;span class="o"&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;OMS_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// whsec_... from the OwnMailServer dashboard&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LISTMONK&lt;/span&gt; &lt;span class="o"&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;LISTMONK_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// https://lists.example.com&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AUTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Basic &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&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;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;LISTMONK_API_USER&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;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;LISTMONK_API_TOKEN&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="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Standard Webhooks signature: base64 HMAC-SHA256 of "id.timestamp.body".&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webhook-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webhook-timestamp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;sigs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webhook-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^whsec_/&lt;/span&gt;&lt;span class="p"&gt;,&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;base64&lt;/span&gt;&lt;span class="dl"&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&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;id&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;ts&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;raw&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="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^v1,/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expected&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="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&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="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&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;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Our "block" bounces (refused by policy) aren't dead addresses, so Listmonk gets them as soft.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email.complained&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;complaint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email.bounced&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bounce&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hard&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;hard&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;soft&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="kc"&gt;null&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;kind&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;r&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;LISTMONK&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/bounce`&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="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="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AUTH&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;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ownmailserver&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;diagnostic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bounce&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;diagnostic&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// a non-2xx reply makes us retry later&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="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&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="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8787&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it next to Listmonk, behind the same HTTPS proxy, for example at &lt;code&gt;https://lists.example.com/oms-webhook&lt;/code&gt;. We only deliver webhooks to public HTTPS addresses. If Listmonk is down, the relay replies &lt;code&gt;502&lt;/code&gt; and we retry for up to 24 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Send your first campaign
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lists&lt;/strong&gt;: create a list. Choose &lt;strong&gt;Double opt-in&lt;/strong&gt; for anything people join through a form. It costs a few signups and saves you from typos, fake addresses and spam traps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscribers → Import&lt;/strong&gt;: upload a CSV of people who agreed to hear from you. If you're moving from another tool, remove its unsubscribed and bounced contacts first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Campaigns → New&lt;/strong&gt;: write the email and pick the list. The From address must be on your verified domain.&lt;/li&gt;
&lt;li&gt;Send a test to yourself and check it lands in the inbox, with the Unsubscribe button next to the sender's name in Gmail.&lt;/li&gt;
&lt;li&gt;Start small. Send the first campaigns to your most engaged readers and grow from there as your cap grows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After it goes out, the Claude connector or the Emails page in your dashboard shows delivery, bounces and complaints per message. Listmonk shows opens, clicks and unsubscribes per campaign. Use one tool's open and click tracking, not both, so links aren't rewritten twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Listmonk isn't the right fit
&lt;/h2&gt;

&lt;p&gt;Listmonk is a lean tool for teams comfortable running a server. It's a poor fit if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nobody on the team wants to run software.&lt;/strong&gt; Listmonk needs updates, backups of its database and an HTTPS proxy. A hosted newsletter service is less work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need complex automations.&lt;/strong&gt; Listmonk handles lists, segments, templates, campaigns and transactional templates. Multi-step journeys with branching are not its strength.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't add the HTTPS domain.&lt;/strong&gt; Without a reachable Root URL, unsubscribe links break, and Gmail and Yahoo may filter your mail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Listmonk also works with any SMTP server, including one you run yourself. Before your first campaign, run your domain through our &lt;a href="https://ownmailserver.com/tools/email-checker" rel="noopener noreferrer"&gt;email checker&lt;/a&gt; and make sure &lt;a href="https://ownmailserver.com/guides/how-to-set-up-dmarc" rel="noopener noreferrer"&gt;DMARC&lt;/a&gt; is in place.&lt;/p&gt;

&lt;p&gt;Sending through OwnMailServer is pay as you go, $0.20 per 1,000 emails. See &lt;a href="https://ownmailserver.com/pricing" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; for the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does OwnMailServer have its own newsletter or campaign editor?
&lt;/h3&gt;

&lt;p&gt;Not yet. Today OwnMailServer sends email over an API and SMTP, and handles warm-up, bounces, complaints, suppressions and monitoring. Contacts, lists and campaign editing aren't built in. Listmonk fills that gap: it stores your subscribers and campaigns and hands each email to us over SMTP. You keep full control of your list, because it lives in your own database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Listmonk free?
&lt;/h3&gt;

&lt;p&gt;Yes. Listmonk is open source under the AGPL-3.0 license and costs nothing to use. You pay for the server you run it on, typically a small VPS, and for sending. You can run it next to an app you already host, since it only needs Docker and a PostgreSQL database, which its Docker Compose file sets up for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did Listmonk pause my campaign?
&lt;/h3&gt;

&lt;p&gt;Listmonk pauses a campaign when sending errors pass its Maximum error threshold. With OwnMailServer, the usual cause is a temporary 450 reply: you sent faster than 10 messages a second, or reached your daily warm-up cap. Lower concurrency and message rate, or set a sliding window limit at your current cap, then resume the campaign. Check your dashboard for the cap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who handles unsubscribes, Listmonk or OwnMailServer?
&lt;/h3&gt;

&lt;p&gt;Listmonk, if you leave its Include List-Unsubscribe header setting on. Listmonk adds the one-click unsubscribe headers and records the unsubscribe on the subscriber. When an email already carries its own List-Unsubscribe header, we don't add ours. We still sign both headers with DKIM, which Gmail and Yahoo require before they show the Unsubscribe button.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when an address bounces?
&lt;/h3&gt;

&lt;p&gt;We add hard bounces and spam complaints to your suppression list automatically. If Listmonk sends to that address again, we drop it, don't send it, and don't count it toward your warm-up cap. To clean the address out of Listmonk too, forward our bounce and complaint webhooks to Listmonk's bounce endpoint with the small script in this guide.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ownmailserver.com/guides/listmonk-smtp-setup" rel="noopener noreferrer"&gt;ownmailserver.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>email</category>
      <category>docker</category>
      <category>newsletter</category>
    </item>
    <item>
      <title>How to send Laravel mail through OwnMailServer</title>
      <dc:creator>Hani Khanani</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:54:24 +0000</pubDate>
      <link>https://dev.to/hani_khanani_2842a21e1b57/how-to-send-laravel-mail-through-ownmailserver-3am7</link>
      <guid>https://dev.to/hani_khanani_2842a21e1b57/how-to-send-laravel-mail-through-ownmailserver-3am7</guid>
      <description>&lt;p&gt;To send Laravel mail through OwnMailServer, set &lt;code&gt;MAIL_MAILER=smtp&lt;/code&gt;, &lt;code&gt;MAIL_HOST=smtp.ownmailserver.com&lt;/code&gt;, &lt;code&gt;MAIL_PORT=587&lt;/code&gt;, any &lt;code&gt;MAIL_USERNAME&lt;/code&gt; and an API key as &lt;code&gt;MAIL_PASSWORD&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;, with &lt;code&gt;MAIL_FROM_ADDRESS&lt;/code&gt; on your verified domain. Leave &lt;code&gt;MAIL_SCHEME&lt;/code&gt; empty so Laravel upgrades the connection with STARTTLS. Your mailables, notifications and password resets then go through us with no code changes.&lt;/p&gt;

&lt;p&gt;This guide uses Laravel 11 to 13, which share the same mail configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An approved OwnMailServer account with a &lt;strong&gt;verified sending domain&lt;/strong&gt;. The From address must be on it, or we refuse the message with &lt;code&gt;550&lt;/code&gt;. The &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; list the DNS records, and the &lt;a href="https://ownmailserver.com/guides/how-to-set-up-dkim" rel="noopener noreferrer"&gt;DKIM guide&lt;/a&gt; explains them.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;API key&lt;/strong&gt;, created in your dashboard under &lt;strong&gt;API keys &amp;amp; SMTP&lt;/strong&gt;. Choose &lt;strong&gt;Sending only&lt;/strong&gt; (the default), limited to your sending domain: SMTP only needs to send. Use separate keys for staging and production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Set the mail settings in .env
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MAIL_MAILER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;smtp
&lt;span class="nv"&gt;MAIL_SCHEME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;null
&lt;span class="nv"&gt;MAIL_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;smtp.ownmailserver.com
&lt;span class="nv"&gt;MAIL_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;587
&lt;span class="nv"&gt;MAIL_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ownmailserver
&lt;span class="nv"&gt;MAIL_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
&lt;span class="nv"&gt;MAIL_FROM_ADDRESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hello@acme.com"&lt;/span&gt;
&lt;span class="nv"&gt;MAIL_FROM_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAIL_SCHEME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;empty or &lt;code&gt;null&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Port 587 starts plain and upgrades with STARTTLS. &lt;code&gt;smtps&lt;/code&gt; is for port 465, which we don't offer.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAIL_USERNAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;any value&lt;/td&gt;
&lt;td&gt;We identify you by the API key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAIL_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an API key&lt;/td&gt;
&lt;td&gt;The same key works for the API.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAIL_FROM_ADDRESS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an address on your verified domain&lt;/td&gt;
&lt;td&gt;The default for every mailable that doesn't set its own.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Laravel 11 replaced the old &lt;code&gt;MAIL_ENCRYPTION&lt;/code&gt; setting with &lt;code&gt;MAIL_SCHEME&lt;/code&gt;. If you upgraded an older app, delete &lt;code&gt;MAIL_ENCRYPTION=tls&lt;/code&gt; from &lt;code&gt;.env&lt;/code&gt;, since it no longer does anything.&lt;/p&gt;

&lt;p&gt;If your deploy runs &lt;code&gt;php artisan config:cache&lt;/code&gt;, Laravel stops reading &lt;code&gt;.env&lt;/code&gt; at runtime, so clear or rebuild the cache and restart the queue workers after changing these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan config:cache
php artisan queue:restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Send a test
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;php artisan tinker&lt;/code&gt; is the quickest check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'It works.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'you@example.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Laravel test'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exception means we accepted it. Look the address up in your OwnMailServer dashboard to see the delivery. If you get an exception, the SMTP code in it is explained in the table at the end of this guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Send real mail with a mailable
&lt;/h2&gt;

&lt;p&gt;Mailables are Laravel's way of describing each email. Generate one with a Markdown template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:mail OrderShipped &lt;span class="nt"&gt;--markdown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mail.orders.shipped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Mail/OrderShipped.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Mail&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Bus\Queueable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\Queue\ShouldQueue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Mail\Mailable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Mail\Mailables\Content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Mail\Mailables\Envelope&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Queue\SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderShipped&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Mailable&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Queueable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SerializesModels&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$tries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nv"&gt;$backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;Order&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;envelope&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Envelope&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Envelope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Your order #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has shipped"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;Content&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;markdown&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'mail.orders.shipped'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Mail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderShipped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the class implements &lt;code&gt;ShouldQueue&lt;/code&gt;, &lt;code&gt;send&lt;/code&gt; puts it on the queue instead of sending during the web request, so a slow SMTP connection never slows a page down. Run a worker with &lt;code&gt;php artisan queue:work&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$tries&lt;/code&gt; and &lt;code&gt;$backoff&lt;/code&gt; properties matter here. Some of our refusals are temporary: &lt;code&gt;450&lt;/code&gt; when you send faster than 10 messages a second or reach your daily warm-up cap. Laravel then throws an exception, and the queue retries the job after 10 seconds, a minute and five minutes instead of dropping the email.&lt;/p&gt;

&lt;p&gt;Notifications sent through the &lt;code&gt;mail&lt;/code&gt; channel and Laravel's built-in password reset and email verification messages use the same mailer, so they go through us too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Keep your old provider as a failover
&lt;/h2&gt;

&lt;p&gt;A new account's daily cap starts at a few hundred recipients and grows about 25% after each clean day (&lt;a href="https://ownmailserver.com/guides/email-ip-warm-up" rel="noopener noreferrer"&gt;how warm-up works&lt;/a&gt;). Laravel's &lt;code&gt;failover&lt;/code&gt; mailer lets you switch now without waiting for that: it tries mailers in order and moves on when one throws.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;config/mail.php&lt;/code&gt;, add your old provider as a second mailer, then list both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'mailers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'smtp'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="cm"&gt;/* OwnMailServer, from .env as above */&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'old'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'smtp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'host'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OLD_MAIL_HOST'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'port'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OLD_MAIL_PORT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'username'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OLD_MAIL_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OLD_MAIL_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;

    &lt;span class="s1"&gt;'failover'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'transport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'failover'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'mailers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'smtp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'old'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'retry_after'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MAIL_MAILER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;failover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now an email we refuse, for example above your cap, goes out through the old provider instead. Watch your dashboard: once the cap is comfortably above your daily volume and the old provider has stopped picking up mail, set &lt;code&gt;MAIL_MAILER=smtp&lt;/code&gt; and cancel it. Your old provider's domain records need to stay in place until then, so its mail still passes SPF and DKIM. Our &lt;a href="https://ownmailserver.com/guides/migrate-from-sendgrid" rel="noopener noreferrer"&gt;SendGrid migration guide&lt;/a&gt; covers the DNS side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option: The HTTP API instead of SMTP
&lt;/h2&gt;

&lt;p&gt;SMTP is the natural fit for Laravel, because every mailable, notification and package already uses it. For a one-off send where you want our response, such as the email id or an idempotency key, call the API with Laravel's HTTP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'services.ownmailserver.key'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withHeaders&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'Idempotency-Key'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"receipt-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://api.ownmailserver.com/emails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'from'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Acme &amp;lt;hello@acme.com&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'to'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'subject'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Receipt for order #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'html'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mail.receipt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$emailId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;'ownmailserver' =&amp;gt; ['key' =&amp;gt; env('OMS_API_KEY')]&lt;/code&gt; to &lt;code&gt;config/services.php&lt;/code&gt;. With the same &lt;code&gt;Idempotency-Key&lt;/code&gt; within 24 hours, we return the first response and send nothing again, so a retried job can't email the customer twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  If sending fails
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exception mentions&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Connection could not be established&lt;/code&gt; / timed out&lt;/td&gt;
&lt;td&gt;Outbound port 587 is blocked&lt;/td&gt;
&lt;td&gt;Allow outbound 587 on your server or host's firewall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;535&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MAIL_PASSWORD&lt;/code&gt; isn't a valid API key&lt;/td&gt;
&lt;td&gt;Paste the full key; clear the config cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;550&lt;/code&gt; domain isn't verified&lt;/td&gt;
&lt;td&gt;The From address is on another domain&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;MAIL_FROM_ADDRESS&lt;/code&gt;, and any mailable's &lt;code&gt;from&lt;/code&gt;, to your verified domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;550&lt;/code&gt; awaiting approval&lt;/td&gt;
&lt;td&gt;Your account is still being reviewed&lt;/td&gt;
&lt;td&gt;Wait for approval, usually within one business day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;450&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 messages a second or the warm-up cap&lt;/td&gt;
&lt;td&gt;Queued mail retries on its own; add failover for the cap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;452&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;More than 50 recipients&lt;/td&gt;
&lt;td&gt;Send to fewer people per message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;552&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Larger than 10 MB&lt;/td&gt;
&lt;td&gt;Link to big files instead of attaching them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After 5 failed logins from one IP address we block that address for 15 minutes, so fix a wrong password before retrying in a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this isn't the right fit
&lt;/h2&gt;

&lt;p&gt;If your app sends a few thousand emails a month, any SMTP provider's free or entry plan will do, and Laravel makes switching between them a change of &lt;code&gt;.env&lt;/code&gt;. We suit apps that want their own IP from 200,000 emails a month, warm-up and monitoring handled, and bounces and complaints suppressed automatically. Sending costs &lt;a href="https://ownmailserver.com/pricing" rel="noopener noreferrer"&gt;$0.20 per 1,000 emails&lt;/a&gt;, with a $10 monthly minimum that covers the first 50,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is MAIL_SCHEME in Laravel?
&lt;/h3&gt;

&lt;p&gt;Laravel 11 replaced MAIL_ENCRYPTION with MAIL_SCHEME. It takes smtp or smtps. Leave it empty, or set smtp, for port 587: Laravel connects in plain text and upgrades with STARTTLS when the server offers it, which ours requires. Use smtps only for port 465, which opens with TLS straight away. If you upgraded an older app, delete MAIL_ENCRYPTION from .env.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does Laravel still use my old mail settings?
&lt;/h3&gt;

&lt;p&gt;Your configuration is probably cached. When you run php artisan config:cache, as most deploy scripts do, Laravel stops reading .env and uses the cached file. After you change the mail settings, run php artisan config:clear, or run config:cache again, and restart your queue workers, which keep the old settings in memory until they restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Laravel's Resend mailer with OwnMailServer?
&lt;/h3&gt;

&lt;p&gt;Use SMTP instead. Laravel's Resend mailer uses Resend's PHP library, which reads its address only from the RESEND_BASE_URL environment variable with getenv. Once your config is cached that variable often isn't set, and the library quietly calls Resend instead. We test Resend's Node.js and Python SDKs against our API, not the PHP one, so SMTP is the dependable route.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I keep my old email provider as a backup?
&lt;/h3&gt;

&lt;p&gt;Define a failover mailer in config/mail.php that lists our SMTP mailer first and your old provider second, then set MAIL_MAILER=failover. Laravel tries each mailer in order and moves to the next one when sending throws an error. During warm-up, mail above your daily cap goes out through the old provider until our cap is high enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test Laravel email without sending it?
&lt;/h3&gt;

&lt;p&gt;In automated tests, call Mail::fake() and assert with Mail::assertSent or assertQueued; nothing is sent. For local development, set MAIL_MAILER=log to write each email to the log, or point SMTP at a local catcher such as Mailpit. Use our SMTP settings only in staging and production, with a separate API key for each.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ownmailserver.com/guides/laravel-smtp-setup" rel="noopener noreferrer"&gt;ownmailserver.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>email</category>
      <category>smtp</category>
    </item>
    <item>
      <title>How to send email from Next.js with OwnMailServer</title>
      <dc:creator>Hani Khanani</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:48:53 +0000</pubDate>
      <link>https://dev.to/hani_khanani_2842a21e1b57/how-to-send-email-from-nextjs-with-ownmailserver-a45</link>
      <guid>https://dev.to/hani_khanani_2842a21e1b57/how-to-send-email-from-nextjs-with-ownmailserver-a45</guid>
      <description>&lt;p&gt;To send email from Next.js, call an email API from server code: a route handler or a Server Action, never a client component. With OwnMailServer that's one &lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;POST https://api.ownmailserver.com/emails&lt;/code&gt;, with your API key read from an environment variable. If your app already uses Resend's SDK or React Email, keep the code: set &lt;code&gt;RESEND_BASE_URL=https://api.ownmailserver.com&lt;/code&gt; and swap in our API key.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;New packages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starting fresh&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetch&lt;/code&gt; to &lt;code&gt;POST /emails&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Already on Resend's SDK&lt;/td&gt;
&lt;td&gt;The same SDK with &lt;code&gt;RESEND_BASE_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want templates as React components&lt;/td&gt;
&lt;td&gt;React Email, with either of the above&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@react-email/components&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A library that only speaks SMTP&lt;/td&gt;
&lt;td&gt;Nodemailer to &lt;code&gt;smtp.ownmailserver.com:587&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nodemailer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The examples use the App Router. The same calls work in the Pages Router's API routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Add your API key
&lt;/h2&gt;

&lt;p&gt;You need an approved OwnMailServer account with a &lt;strong&gt;verified sending domain&lt;/strong&gt;: the &lt;code&gt;from&lt;/code&gt; address must be on it, or the API returns &lt;code&gt;403&lt;/code&gt;. Create an API key in your dashboard under &lt;strong&gt;API keys &amp;amp; SMTP&lt;/strong&gt;. Choose &lt;strong&gt;Sending only&lt;/strong&gt; (the default), limited to your sending domain: every call in this guide only sends. Then add it to &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OMS_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't prefix it with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt;. Next.js inlines &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; variables into the JavaScript it sends to browsers, and even a sending-only key can send email as your domain. Add the same variable in your host's settings for production, such as Vercel's &lt;strong&gt;Environment Variables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To make a mistake fail at build time rather than leak, put the send helper in a file that imports the &lt;code&gt;server-only&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/email.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server-only&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Email&lt;/span&gt; &lt;span class="o"&gt;=&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="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;replyTo&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&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="nx"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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://api.ownmailserver.com/emails&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;OMS_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="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme &amp;lt;hello@acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&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;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&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="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;html&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="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reply_to&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="nx"&gt;replyTo&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;ok&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;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;body&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="kr"&gt;string&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;If a client component imports this file, the build fails instead of shipping your key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Send from a route handler
&lt;/h2&gt;

&lt;p&gt;A route handler is the right place for anything another service calls, or a form that posts JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/contact/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;name&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="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;request&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid input&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sales@acme.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;replyTo&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="s2"&gt;`New enquiry from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&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="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;escapeHtml&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;&amp;lt;/p&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&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="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;escapeHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;&amp;amp;&amp;lt;&amp;gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;&amp;amp;&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;&amp;amp;amp;&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;&amp;lt;&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;&amp;amp;lt;&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;&amp;gt;&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;&amp;amp;gt;&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="s1"&gt;"&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;&amp;amp;quot;&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;'&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;&amp;amp;#39;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&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 form emails &lt;strong&gt;you&lt;/strong&gt;, with the visitor's address in &lt;code&gt;reply_to&lt;/code&gt;, so pressing Reply answers them. Never send to an address the visitor types, or from it: an open form that emails anyone is a spam relay, and a visitor's address in &lt;code&gt;from&lt;/code&gt; is refused because it isn't your verified domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Or send from a Server Action
&lt;/h2&gt;

&lt;p&gt;For a form inside your app, a Server Action skips the API route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/invite/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sendEmail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// your own session helper&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendInvite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&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;user&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;auth&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not signed in&lt;/span&gt;&lt;span class="dl"&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;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&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;inviteId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// in a real app, the id of the invite row you just saved&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;to&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; invited you to Acme`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;p&amp;gt;&amp;lt;a href="https://acme.com/invite/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inviteId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;Accept the invite&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="s2"&gt;`invite-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inviteId&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/invite/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sendInvite&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./actions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sendInvite&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Send invite&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Server Actions are public endpoints that anyone can call, so check the session inside the action, as above, not only in the page.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Idempotency-Key&lt;/code&gt; makes retries safe. If the same key arrives again within 24 hours, our API returns the first response and sends nothing, so a double-click, a retried request or a re-run job can't email someone twice. Use something tied to the event, like the invite or order id.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Resend's SDK or React Email
&lt;/h2&gt;

&lt;p&gt;Our API follows Resend's request shapes, response shapes and errors, and we test Resend's own &lt;a href="https://www.npmjs.com/package/resend" rel="noopener noreferrer"&gt;Node.js SDK&lt;/a&gt;, version 6, against it. If your app already uses it, change the environment, not the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   &lt;span class="c"&gt;# an OwnMailServer key&lt;/span&gt;
&lt;span class="nv"&gt;RESEND_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.ownmailserver.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React Email templates keep working. When you pass &lt;code&gt;react&lt;/code&gt;, the SDK renders the component to HTML on your server and sends the HTML, so nothing depends on Resend's servers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/welcome/route.tsx (JSX, so .tsx)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Resend&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WelcomeEmail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/emails/welcome&lt;/span&gt;&lt;span class="dl"&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;resend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Resend&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;RESEND_API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&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;request&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme &amp;lt;hello@acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;Welcome to Acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;react&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WelcomeEmail&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`welcome-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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;span class="k"&gt;if &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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendering needs &lt;code&gt;@react-email/components&lt;/code&gt; (or &lt;code&gt;@react-email/render&lt;/code&gt;) installed. Without the SDK, call React Email's &lt;code&gt;render()&lt;/code&gt; yourself and pass the HTML to &lt;code&gt;sendEmail()&lt;/code&gt; from Step 1. The full switch, including webhooks and domains, is in &lt;a href="https://ownmailserver.com/guides/migrate-from-resend" rel="noopener noreferrer"&gt;moving from Resend&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  SMTP with Nodemailer
&lt;/h2&gt;

&lt;p&gt;If a library you can't change only speaks SMTP, Nodemailer works in the Node.js runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nodemailer&lt;/span&gt;&lt;span class="dl"&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;transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smtp.ownmailserver.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// starts plain, then upgrades with STARTTLS&lt;/span&gt;
  &lt;span class="na"&gt;requireTLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ownmailserver&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pass&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;OMS_API_KEY&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;On serverless hosts, prefer the API. Every function call would open a new SMTP connection and log in, and an error comes back as an SMTP reply code instead of JSON. One HTTPS request is quicker and easier to debug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and errors to handle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10 requests a second&lt;/strong&gt; per account. A burst gets &lt;code&gt;429 rate_limit_exceeded&lt;/code&gt;; wait for the seconds in the &lt;code&gt;retry-after&lt;/code&gt; header, or send up to 100 emails in one &lt;code&gt;POST /emails/batch&lt;/code&gt; request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The warm-up cap.&lt;/strong&gt; A new account's daily limit starts at a few hundred recipients and grows after each clean day (&lt;a href="https://ownmailserver.com/guides/email-ip-warm-up" rel="noopener noreferrer"&gt;how warm-up works&lt;/a&gt;). Above it, sends get &lt;code&gt;429 daily_quota_exceeded&lt;/code&gt; until the last 24 hours have room again. Move your most important email first, and keep the old provider for bulk sends until the cap is high enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suppressed addresses.&lt;/strong&gt; We don't send to addresses that hard-bounced or complained. The email is stored as &lt;code&gt;suppressed&lt;/code&gt;, so check its status or use webhooks if your app needs to know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accepted isn't delivered.&lt;/strong&gt; A success response means we queued the email. To act on deliveries and bounces, add a &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;webhook&lt;/a&gt; or look the email up with &lt;code&gt;GET /emails/{id}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When this isn't the right fit
&lt;/h2&gt;

&lt;p&gt;If your app sends a few hundred emails a month, Resend's free tier, 3,000 emails a month as of October 2026, may be all you need, and Resend's developer experience is excellent. We suit apps that want their own IP once they pass 200,000 emails a month, managed warm-up and monitoring, and a Resend-compatible API, so switching back and forth is a change of environment variables. Sending through us costs &lt;a href="https://ownmailserver.com/pricing" rel="noopener noreferrer"&gt;$0.20 per 1,000 emails&lt;/a&gt;, with a $10 monthly minimum that covers the first 50,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I send email from Next.js?
&lt;/h3&gt;

&lt;p&gt;Call an email API from server code. In the App Router, that's a route handler such as app/api/contact/route.ts or a Server Action marked use server. Read the API key from an environment variable without the NEXT_PUBLIC_ prefix, so Next.js never sends it to the browser, and make one POST request to the provider's send endpoint. The response tells you the email was accepted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Resend's SDK with OwnMailServer?
&lt;/h3&gt;

&lt;p&gt;Yes. Our API follows Resend's request and response shapes, and we test Resend's own Node.js SDK, version 6, against it. Set RESEND_BASE_URL=&lt;a href="https://api.ownmailserver.com" rel="noopener noreferrer"&gt;https://api.ownmailserver.com&lt;/a&gt;, or pass baseUrl when you create the client, and use an OwnMailServer API key. Sending, batches, attachments, scheduling and idempotency keys all work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does React Email work without Resend?
&lt;/h3&gt;

&lt;p&gt;Yes. React Email is an open-source library that turns React components into email HTML. Call its render function and send the HTML to any provider. If you use Resend's SDK with the react option, the SDK does that rendering on your server before it sends, so the same code works when it points at our API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I use Nodemailer and SMTP in Next.js?
&lt;/h3&gt;

&lt;p&gt;You can, in the Node.js runtime. On serverless hosts such as Vercel, the HTTPS API is usually simpler: each email is one request, with nothing to keep open between function calls, and errors come back as JSON. Use SMTP when a library you can't change only speaks SMTP. Our SMTP server is smtp.ownmailserver.com on port 587.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I stop a contact form from being abused?
&lt;/h3&gt;

&lt;p&gt;Validate the input on the server, send the message to yourself rather than to an address the visitor types, put the visitor's address in reply_to, and rate-limit the route by IP. Add a CAPTCHA or a hidden honeypot field if bots find it. An open form that emails any address you're given will be found and used for spam.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ownmailserver.com/guides/nextjs-send-email" rel="noopener noreferrer"&gt;ownmailserver.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>email</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How to send Firebase email through OwnMailServer</title>
      <dc:creator>Hani Khanani</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:43:22 +0000</pubDate>
      <link>https://dev.to/hani_khanani_2842a21e1b57/how-to-send-firebase-email-through-ownmailserver-5gc2</link>
      <guid>https://dev.to/hani_khanani_2842a21e1b57/how-to-send-firebase-email-through-ownmailserver-5gc2</guid>
      <description>&lt;p&gt;Firebase doesn't ship a list of email providers, but it sends through any SMTP server or HTTPS API, so it works with OwnMailServer in three ways. Use &lt;strong&gt;custom SMTP in Firebase Auth&lt;/strong&gt; for password resets and verification emails, the &lt;strong&gt;Trigger Email from Firestore&lt;/strong&gt; extension to send by writing documents, or a &lt;strong&gt;Cloud Function that calls our API&lt;/strong&gt; for full control. Most apps use the first plus one of the other two.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to send&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;Code needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Password resets, email verification, sign-in links&lt;/td&gt;
&lt;td&gt;Firebase Auth custom SMTP&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emails written as Firestore documents&lt;/td&gt;
&lt;td&gt;Trigger Email extension, or the function below&lt;/td&gt;
&lt;td&gt;None / about 40 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receipts, alerts, anything with your own template&lt;/td&gt;
&lt;td&gt;A Cloud Function calling &lt;code&gt;POST /emails&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;About 40 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fully custom auth emails&lt;/td&gt;
&lt;td&gt;A function with &lt;code&gt;generatePasswordResetLink&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;About 30 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What you need before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An approved OwnMailServer account with a &lt;strong&gt;verified sending domain&lt;/strong&gt;. Every From address must be on that domain, or we refuse the email with &lt;code&gt;550&lt;/code&gt;. The &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; list the DNS records, and the &lt;a href="https://ownmailserver.com/guides/how-to-set-up-dkim" rel="noopener noreferrer"&gt;DKIM guide&lt;/a&gt; explains them.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;API key&lt;/strong&gt;, created in your dashboard under &lt;strong&gt;API keys &amp;amp; SMTP&lt;/strong&gt;. Choose &lt;strong&gt;Sending only&lt;/strong&gt; (the default), limited to your sending domain: it can send and nothing else. It's the SMTP password and the API bearer token. Create one per use, so you can revoke one without breaking the others.&lt;/li&gt;
&lt;li&gt;For Cloud Functions or extensions: a Firebase project on the &lt;strong&gt;Blaze&lt;/strong&gt; plan and the Firebase CLI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Option 1: Firebase Auth emails over custom SMTP
&lt;/h2&gt;

&lt;p&gt;Firebase Auth sends its own emails for password resets, email address verification, email link sign-in and email change. By default they come from a Firebase sender. Point them at us and they come from your domain, signed with your DKIM key, with bounces in your dashboard.&lt;/p&gt;

&lt;p&gt;In the Firebase console, open &lt;strong&gt;Authentication&lt;/strong&gt;, then &lt;strong&gt;Templates&lt;/strong&gt;, then &lt;strong&gt;SMTP settings&lt;/strong&gt;, turn it on and enter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sender address&lt;/td&gt;
&lt;td&gt;An address on your verified domain, such as &lt;code&gt;no-reply@acme.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMTP server host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;smtp.ownmailserver.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;587&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ownmailserver&lt;/code&gt; (we accept any value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;Your API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security mode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;STARTTLS&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Save, then trigger a password reset for a test account. If nothing arrives, look the address up in your dashboard first. A &lt;code&gt;535&lt;/code&gt; means the password isn't a valid API key, and a &lt;code&gt;550&lt;/code&gt; means the sender domain isn't verified. If the message isn't there at all, turn SMTP off and try again: if Firebase's own sender then delivers, the settings are the problem.&lt;/p&gt;

&lt;p&gt;Firebase still writes these emails and their links, so you edit the text in the same &lt;strong&gt;Templates&lt;/strong&gt; screen. If you've upgraded to Identity Platform with multi-tenancy, Google's docs say tenants must be set to inherit custom SMTP settings, or their users keep getting the default sender.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: The Trigger Email from Firestore extension
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://extensions.dev/extensions/firebase/firestore-send-email" rel="noopener noreferrer"&gt;Trigger Email extension&lt;/a&gt; (&lt;code&gt;firebase/firestore-send-email&lt;/code&gt;) watches a collection. Each new document becomes an email, and the extension writes the delivery state back onto the document.&lt;/p&gt;

&lt;p&gt;Install it from the console or the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase ext:install firebase/firestore-send-email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it asks for settings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authentication Type&lt;/td&gt;
&lt;td&gt;Username &amp;amp; Password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMTP connection URI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;smtp://ownmailserver@smtp.ownmailserver.com:587&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMTP password&lt;/td&gt;
&lt;td&gt;Your API key (stored in Secret Manager)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email documents collection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mail&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default FROM address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Acme &amp;lt;no-reply@acme.com&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use &lt;code&gt;smtp://&lt;/code&gt; with port 587, not &lt;code&gt;smtps://&lt;/code&gt;: the connection starts plain and upgrades with STARTTLS, which we require before login. Then send an email by adding a document:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getFirestore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;addDoc&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase/firestore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getFirestore&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mail&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;to&lt;/span&gt;&lt;span class="p"&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;jane@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&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 receipt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Thanks for your order.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Thanks for your order.&amp;lt;/p&amp;gt;&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two cautions. If clients can write to &lt;code&gt;mail&lt;/code&gt;, anyone signed in to your app can email anyone, so write these documents from trusted server code or lock the collection down with security rules. And Firebase says the &lt;a href="https://firebase.google.com/docs/extensions/faq-and-troubleshooting" rel="noopener noreferrer"&gt;Extensions service shuts down on March 31, 2027&lt;/a&gt;: installed extensions keep running, but you can't reconfigure or update them after that. For a new project, use Option 3, which reads the same document shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: A Cloud Function that calls our API
&lt;/h2&gt;

&lt;p&gt;A second-generation Cloud Function can call &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;&lt;code&gt;POST /emails&lt;/code&gt;&lt;/a&gt; with plain &lt;code&gt;fetch&lt;/code&gt;, which Node.js 18 and later include. This one replaces the extension: it sends every new &lt;code&gt;mail&lt;/code&gt; document and records the result on it.&lt;/p&gt;

&lt;p&gt;Store the API key as a secret first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase functions:secrets:set OMS_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions/index.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onDocumentCreated&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-functions/v2/firestore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineSecret&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-functions/params&lt;/span&gt;&lt;span class="dl"&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;OMS_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OMS_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendMail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onDocumentCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mail/{id}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;OMS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&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://api.ownmailserver.com/emails&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;OMS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&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="c1"&gt;// Firestore triggers can run more than once; the key stops a second send.&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`mail-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&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="s2"&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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme &amp;lt;no-reply@acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&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;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&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="nx"&gt;doc&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="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&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="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&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="nx"&gt;text&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ERROR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;body&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;span class="c1"&gt;// 429 and 5xx are worth retrying; turn on retries in the function's settings.&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;429&lt;/span&gt; &lt;span class="o"&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;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&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="nx"&gt;body&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="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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;delivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SENT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&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="p"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;firebase deploy &lt;span class="nt"&gt;--only&lt;/span&gt; functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Idempotency-Key&lt;/code&gt; header matters here. Firestore triggers deliver at least once, so a function can run twice for one document. With the same key within 24 hours, our API returns the first response and sends nothing again.&lt;/p&gt;

&lt;p&gt;From here, the rest of the API works the same way: &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;scheduled sends&lt;/a&gt;, batches of up to 100, tags and webhooks for deliveries and bounces. Our API also follows Resend's request shapes, so if you already call Resend from a function, you can keep the code and &lt;a href="https://ownmailserver.com/guides/migrate-from-resend" rel="noopener noreferrer"&gt;change the base URL&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 4: Custom auth emails with your own template
&lt;/h2&gt;

&lt;p&gt;If you want auth emails in your own design, let the Admin SDK create the link and send it yourself:&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="c1"&gt;// Same functions/index.js as above, so OMS_API_KEY is already defined.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onCall&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-functions/v2/https&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;initializeApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-admin/app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase-admin/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;initializeApp&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestPasswordReset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;onCall&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;OMS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;link&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;getAuth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;generatePasswordResetLink&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="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://acme.com/login&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="k"&gt;catch&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="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// Don't reveal whether the account exists.&lt;/span&gt;
  &lt;span class="p"&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://api.ownmailserver.com/emails&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;OMS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&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="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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme &amp;lt;no-reply@acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;Reset your Acme password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;p&amp;gt;Click to choose a new password:&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;&amp;lt;a href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;Reset password&amp;lt;/a&amp;gt;&amp;lt;/p&amp;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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;&lt;code&gt;generateEmailVerificationLink&lt;/code&gt; and &lt;code&gt;generateSignInWithEmailLink&lt;/code&gt; work the same way. Call this function from your app instead of the client SDK's &lt;code&gt;sendPasswordResetEmail&lt;/code&gt;. It's a public endpoint, so protect it with &lt;a href="https://firebase.google.com/docs/app-check" rel="noopener noreferrer"&gt;App Check&lt;/a&gt; or a rate limit, or it becomes a way to flood inboxes from your domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits to plan for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10 requests or messages a second&lt;/strong&gt; per account. A burst from a batch job gets &lt;code&gt;429&lt;/code&gt; from the API or &lt;code&gt;450&lt;/code&gt; over SMTP; retry with backoff, or use &lt;code&gt;POST /emails/batch&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm-up cap.&lt;/strong&gt; A new account's daily limit starts at a few hundred recipients and rises after each clean day (&lt;a href="https://ownmailserver.com/guides/email-ip-warm-up" rel="noopener noreferrer"&gt;how warm-up works&lt;/a&gt;). Password resets count, so move your highest-volume email last, and keep your old provider for it until the cap is high enough.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;50 recipients and 10 MB per message.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suppressions.&lt;/strong&gt; We drop email to addresses that hard-bounced or complained, and don't count it toward your cap. Fix typos in sign-up forms early, since every bad address bounces.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When this isn't the right fit
&lt;/h2&gt;

&lt;p&gt;If you send a few hundred auth emails a month, Firebase's default sender is free and you may not need any of this. The reasons to switch are your own domain on every email, bounce and complaint data you can see, and one provider for auth and app email. At our &lt;a href="https://ownmailserver.com/pricing" rel="noopener noreferrer"&gt;pay-as-you-go pricing&lt;/a&gt;, the $10 monthly minimum covers the first 50,000 emails.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Firebase integrate with OwnMailServer?
&lt;/h3&gt;

&lt;p&gt;Yes, through standard SMTP and HTTPS rather than a listed integration. Firebase Auth can send password reset and verification emails through any custom SMTP server, the Trigger Email from Firestore extension accepts any SMTP connection URI, and Cloud Functions can call our REST API with fetch. All three take a few minutes once your sending domain is verified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Firebase Auth send password reset emails through my own SMTP server?
&lt;/h3&gt;

&lt;p&gt;Yes. In the Firebase console, open Authentication, then Templates, then SMTP settings. Turn it on and enter a sender address on your verified domain, smtp.ownmailserver.com, port 587, STARTTLS, any username and an API key as the password. Firebase keeps its own templates and links; we deliver the email and record bounces. Confirm the menu names in your console, since Google moves them occasionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the Trigger Email from Firestore extension going away?
&lt;/h3&gt;

&lt;p&gt;Firebase says the Extensions service is deprecated and shuts down on March 31, 2027. Installed extensions keep running, but after that date you can't update, reconfigure or uninstall them. If you use Trigger Email today it keeps working, but for a new project a short Cloud Function that calls our API is the safer choice, and this guide includes one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need the Firebase Blaze plan?
&lt;/h3&gt;

&lt;p&gt;For Cloud Functions and extensions, yes: both need the pay-as-you-go Blaze plan, and functions can't reach outside services without it. Firebase Auth's SMTP setting doesn't depend on Cloud Functions. Firebase's own usage charges are separate from ours, which are $0.20 per 1,000 emails with a $10 monthly minimum.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I put the API key in my Firebase web or mobile app?
&lt;/h3&gt;

&lt;p&gt;No. Even a sending-only key can send email as your domain, and a full-access key can also manage your account, so it belongs on the server only. Keep it in Secret Manager through defineSecret, and have the app write a Firestore document or call a callable function instead. Lock that path down with security rules or App Check so strangers can't use your app to send email.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ownmailserver.com/guides/firebase-email-setup" rel="noopener noreferrer"&gt;ownmailserver.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>email</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to turn TradingView alerts into futures orders with a webhook</title>
      <dc:creator>Hani Khanani</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:39:18 +0000</pubDate>
      <link>https://dev.to/hani_khanani_2842a21e1b57/how-to-turn-tradingview-alerts-into-futures-orders-with-a-webhook-4ii9</link>
      <guid>https://dev.to/hani_khanani_2842a21e1b57/how-to-turn-tradingview-alerts-into-futures-orders-with-a-webhook-4ii9</guid>
      <description>&lt;p&gt;TradingView can't place futures orders at your broker by itself, but every alert can POST a message to a URL. Put a small server behind that URL, have it read the message and call your broker's API, and your alerts become orders. This post covers how TradingView sends webhooks, how to write the alert message, and the problems a receiver has to handle before you trust it with a live account.&lt;/p&gt;

&lt;h2&gt;
  
  
  How TradingView sends a webhook
&lt;/h2&gt;

&lt;p&gt;When an alert fires and &lt;strong&gt;Webhook URL&lt;/strong&gt; is ticked in the alert's notifications, TradingView sends an HTTP POST to that URL with the alert's &lt;strong&gt;Message&lt;/strong&gt; as the body. As of October 2026, TradingView's help page on webhooks says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webhook alerts only work with &lt;strong&gt;2-factor authentication&lt;/strong&gt; turned on for your TradingView account.&lt;/li&gt;
&lt;li&gt;Only ports &lt;strong&gt;80 and 443&lt;/strong&gt; are accepted. &lt;code&gt;https://example.com:8443/hook&lt;/code&gt; won't work.&lt;/li&gt;
&lt;li&gt;If your server takes longer than &lt;strong&gt;3 seconds&lt;/strong&gt; to respond, the request is cancelled.&lt;/li&gt;
&lt;li&gt;If the message is valid JSON, the request has &lt;code&gt;Content-Type: application/json&lt;/code&gt;. Otherwise it's &lt;code&gt;text/plain&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;IPv6 isn't supported, so your hostname needs an A record.&lt;/li&gt;
&lt;li&gt;Requests come from four published IP addresses (52.89.214.238, 34.212.75.30, 54.218.53.128 and 52.32.178.7), which you can allowlist.&lt;/li&gt;
&lt;li&gt;Webhooks can occasionally fail to arrive. The alert log has a &lt;strong&gt;Webhook status&lt;/strong&gt; column for checking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TradingView also says not to put passwords or other credentials in the message body. That matters later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the alert message
&lt;/h2&gt;

&lt;p&gt;Make the message JSON, so it arrives as &lt;code&gt;application/json&lt;/code&gt; and your server can parse it without guessing. A minimal order looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"buy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MNQ1!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"market"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't want to hard-code &lt;code&gt;buy&lt;/code&gt; and &lt;code&gt;1&lt;/code&gt; for a strategy that both enters and exits. TradingView fills in placeholders in double curly braces when the alert fires. For a Pine Script &lt;code&gt;strategy()&lt;/code&gt;, create the alert on the strategy itself with the trigger &lt;strong&gt;Order fills only&lt;/strong&gt;, and use:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  "action": "{{strategy.order.action}}",
  "symbol": "{{ticker}}",
  "qty": {{strategy.order.contracts}},
  "orderType": "market"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;On each fill, &lt;code&gt;strategy.order.action&lt;/code&gt; becomes &lt;code&gt;buy&lt;/code&gt; or &lt;code&gt;sell&lt;/code&gt;, &lt;code&gt;strategy.order.contracts&lt;/code&gt; becomes the number of contracts and &lt;code&gt;ticker&lt;/code&gt; becomes the chart's symbol. Leave &lt;code&gt;qty&lt;/code&gt; unquoted so it arrives as a number.&lt;/p&gt;

&lt;p&gt;These &lt;code&gt;strategy.*&lt;/code&gt; placeholders don't exist for an &lt;code&gt;indicator()&lt;/code&gt;. There you either write fixed values into each alert, or build the whole message in Pine and send it with &lt;code&gt;alert()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;msg = '{"action":"' + (isLong ? "buy" : "sell") + '","symbol":"' + syminfo.ticker +
      '","qty":' + str.tostring(qty) + ',"orderType":"market"}'
alert(msg, alert.freq_once_per_bar_close)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A minimal receiver
&lt;/h2&gt;

&lt;p&gt;Here's the shape of a receiver in Node.js with Express. The secret is in the URL path rather than the body, so each strategy gets its own URL that you can revoke, and &lt;code&gt;placeOrder&lt;/code&gt; stands for your broker's API call:&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;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// body -&amp;gt; time, to drop duplicate deliveries&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/hook/:token&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="nx"&gt;req&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="o"&gt;=&amp;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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&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;HOOK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&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;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;market&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&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="o"&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;buy&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;sell&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bad alert message&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// Answer TradingView first, then trade: the broker call may take longer than 3 seconds.&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;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderType&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// behind a reverse proxy on 443 with a real TLS certificate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's short, but each line answers one of the problems below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a receiver has to get right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The 3-second limit.&lt;/strong&gt; A broker API call that has to log in, refresh a token or wait for an acknowledgement can easily take longer than 3 seconds. Answer TradingView first and place the order afterwards, or better, put it on a queue with retries. Then log the broker's answer somewhere you'll actually look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicates.&lt;/strong&gt; Several alerts can fire on the same bar, and a retry can deliver the same message twice. A receiver that turns every POST into an order will one day double your position. The 5-second window above is the bare minimum. A real one records each order it placed, and the broker's own idempotency key if it has one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contract rollover.&lt;/strong&gt; &lt;code&gt;MNQ1!&lt;/code&gt; is TradingView's continuous contract, not something a futures broker can trade. Your receiver has to map it to the current front-month contract, such as &lt;code&gt;MNQZ6&lt;/code&gt;, and keep doing that correctly after every quarterly roll. Hard-coding the month works until the day it silently sends orders to an expiring contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exits.&lt;/strong&gt; "Close my position" is less simple than it sounds. Flattening the whole account also cancels resting orders and closes positions in other symbols that your alert never mentioned. Decide which one you mean, and to exit one symbol, send the opposite side for that quantity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brackets.&lt;/strong&gt; If you send a take-profit and a stop-loss with the entry, they have to be linked as one-cancels-other (OCO) at the broker. Otherwise, when the target fills, the stop is still working and can open a new position the wrong way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accounts that stop existing.&lt;/strong&gt; Prop-firm evaluation accounts get reset, breached or replaced. The alert keeps firing at an account the broker no longer reports, and every order is rejected. If nothing tells you, you find out at the end of the day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth and logging.&lt;/strong&gt; Use HTTPS, keep the token out of the message body, and log every alert next to the order it produced, or the error it got. When a trade looks wrong, that log is the only way to tell whether TradingView, your receiver or the broker caused it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test on a simulated account first.&lt;/strong&gt; Run the whole path, from alert to fill, on a demo or sim account until the logs match your strategy's trades.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shortcut: a hosted webhook
&lt;/h2&gt;

&lt;p&gt;If you'd rather not run and maintain that server, this is what we built into &lt;a href="https://signaltradeapp.com" rel="noopener noreferrer"&gt;Signal Trade App&lt;/a&gt;, a futures trade copier for Tradovate, Rithmic, NinjaTrader and ProjectX accounts. You create a webhook token on the Signals page, paste its URL into the TradingView alert, and use the same JSON as above:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
  "action": "{{strategy.order.action}}",
  "symbol": "{{ticker}}",
  "qty": {{strategy.order.contracts}},
  "orderType": "market",
  "takeProfit": 4550.00,
  "stopLoss": 4490.00
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It handles the problems above like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuous symbols such as &lt;code&gt;MNQ1!&lt;/code&gt; or &lt;code&gt;ES1!&lt;/code&gt; resolve to the current front-month contract, including after rollovers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;takeProfit&lt;/code&gt; and &lt;code&gt;stopLoss&lt;/code&gt; work with market, limit and stop entries, and the two exit legs are linked as OCO at the broker.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{"action": "close"}&lt;/code&gt; flattens the whole account. If you also name a symbol while the account holds a different one, the close is refused rather than closing a position the alert never mentioned.&lt;/li&gt;
&lt;li&gt;A token is tied to one account. If the broker stops reporting that account, the Signals page flags the token instead of failing silently.&lt;/li&gt;
&lt;li&gt;If the token points at a leader account, the order is copied to its follower accounts with each follower's own ratio and risk limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are limits: one take-profit and one stop-loss per entry, no trailing or breakeven stops from a single webhook, and no OCO between two separate entry orders. For those you'd use the &lt;a href="https://signaltradeapp.com/api-docs" rel="noopener noreferrer"&gt;REST API&lt;/a&gt;. The &lt;a href="https://signaltradeapp.com/tradingview-trade-copier" rel="noopener noreferrer"&gt;TradingView trade copier page&lt;/a&gt; has the full setup.&lt;/p&gt;

&lt;p&gt;Whichever route you take, keep the alert message as JSON, test on a sim account first, and keep a log of every alert next to the order it produced.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Automated trading can lose money quickly. Nothing here is trading advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>trading</category>
      <category>javascript</category>
      <category>api</category>
    </item>
    <item>
      <title>How to set up Supabase custom SMTP with OwnMailServer</title>
      <dc:creator>Hani Khanani</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:30:53 +0000</pubDate>
      <link>https://dev.to/hani_khanani_2842a21e1b57/how-to-set-up-supabase-custom-smtp-with-ownmailserver-4070</link>
      <guid>https://dev.to/hani_khanani_2842a21e1b57/how-to-set-up-supabase-custom-smtp-with-ownmailserver-4070</guid>
      <description>&lt;p&gt;Every production Supabase project needs custom SMTP, because the built-in sender only emails your own team and allows 2 messages an hour. To send through OwnMailServer, open your project's &lt;strong&gt;Authentication&lt;/strong&gt; settings, go to &lt;strong&gt;SMTP&lt;/strong&gt;, and enter &lt;code&gt;smtp.ownmailserver.com&lt;/code&gt;, port &lt;code&gt;587&lt;/code&gt;, any username and an API key as the password, with a From address on your verified domain. Then raise Supabase's 30-an-hour starting limit to match your volume. It takes about ten minutes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom SMTP&lt;/td&gt;
&lt;td&gt;Most projects: Supabase's templates, sent from your domain&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send Email Hook + our API&lt;/td&gt;
&lt;td&gt;Your own templates, languages per user, logging&lt;/td&gt;
&lt;td&gt;One Edge Function&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why Supabase needs custom SMTP
&lt;/h2&gt;

&lt;p&gt;Supabase Auth sends email for signup confirmation, magic links, password resets, invitations, email changes and reauthentication. Out of the box it uses a shared sender that, as of October 2026, Supabase documents like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It only delivers to addresses in your organization's &lt;strong&gt;Team&lt;/strong&gt; settings. Anyone else gets &lt;code&gt;Email address not authorized&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It allows &lt;strong&gt;2 messages an hour&lt;/strong&gt;, and Supabase says this can change without notice.&lt;/li&gt;
&lt;li&gt;It has no delivery or uptime guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's enough to try Supabase, not to run an app. With custom SMTP, sign-in emails come from your domain, signed with your DKIM key, and every bounce and complaint shows up in your dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An approved OwnMailServer account with a &lt;strong&gt;verified sending domain&lt;/strong&gt;. The From address must be on that domain, or we refuse the email with &lt;code&gt;550&lt;/code&gt;. The &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; list the DNS records, and the &lt;a href="https://ownmailserver.com/guides/how-to-set-up-dkim" rel="noopener noreferrer"&gt;DKIM guide&lt;/a&gt; explains them.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;API key&lt;/strong&gt;, created in your dashboard under &lt;strong&gt;API keys &amp;amp; SMTP&lt;/strong&gt;. Choose &lt;strong&gt;Sending only&lt;/strong&gt; (the default), limited to your sending domain, so the key Supabase stores can't do anything but send. Make one just for Supabase, so you can revoke it without touching your other apps.&lt;/li&gt;
&lt;li&gt;Ideally a &lt;strong&gt;separate From address or subdomain for auth email&lt;/strong&gt;, such as &lt;code&gt;no-reply@auth.acme.com&lt;/code&gt;. Supabase recommends this too: if a marketing campaign draws complaints, your password resets aren't caught up in it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Enter the SMTP settings
&lt;/h2&gt;

&lt;p&gt;In the Supabase dashboard, open your project, then &lt;strong&gt;Authentication&lt;/strong&gt;, then the &lt;strong&gt;SMTP&lt;/strong&gt; settings, and turn on custom SMTP:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sender email&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;no-reply@auth.acme.com&lt;/code&gt; (on your verified domain)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sender name&lt;/td&gt;
&lt;td&gt;Your app's name, such as &lt;code&gt;Acme&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;smtp.ownmailserver.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;587&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ownmailserver&lt;/code&gt; (we accept any value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;Your API key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Save. From now on Supabase can email any address. Supabase connects on 587, upgrades with STARTTLS and logs in, which is what we require.&lt;/p&gt;

&lt;p&gt;To set the same thing from a script or CI, use Supabase's Management API with an access token from your account settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; PATCH &lt;span class="s2"&gt;"https://api.supabase.com/v1/projects/&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_REF&lt;/span&gt;&lt;span class="s2"&gt;/config/auth"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$SUPABASE_ACCESS_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "external_email_enabled": true,
    "smtp_admin_email": "no-reply@auth.acme.com",
    "smtp_sender_name": "Acme",
    "smtp_host": "smtp.ownmailserver.com",
    "smtp_port": "587",
    "smtp_user": "ownmailserver",
    "smtp_pass": "'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OMS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Raise the rate limit, in step with warm-up
&lt;/h2&gt;

&lt;p&gt;When you turn on custom SMTP, Supabase sets a deliberately low limit: &lt;strong&gt;30 auth emails an hour&lt;/strong&gt;. Change it on the &lt;strong&gt;Rate Limits&lt;/strong&gt; page in the Authentication settings.&lt;/p&gt;

&lt;p&gt;Two limits now apply, and the lower one wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supabase's hourly limit&lt;/strong&gt;, which you set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Our daily warm-up cap.&lt;/strong&gt; A new account starts at a few hundred recipients a day and rises about 25% after each clean day (&lt;a href="https://ownmailserver.com/guides/email-ip-warm-up" rel="noopener noreferrer"&gt;how warm-up works&lt;/a&gt;). Your dashboard shows today's cap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Supabase allows more than our cap, we answer with a temporary &lt;code&gt;450&lt;/code&gt; and Supabase reports a sending error. So raise Supabase's limit to roughly your cap divided by the hours your users are active, and raise it again as the cap grows. Auth email is usually low volume, so most projects never get close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Test it
&lt;/h2&gt;

&lt;p&gt;Sign up with an address that isn't on your Supabase team, or trigger a reset:&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetPasswordForEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jane@example.com&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;redirectTo&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://acme.com/update-password&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then search for the address in your OwnMailServer dashboard to see the delivery, or ask the &lt;a href="https://ownmailserver.com/claude-email-connector" rel="noopener noreferrer"&gt;Claude connector&lt;/a&gt; what happened to it. If nothing arrived:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You see&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supabase: &lt;code&gt;Email address not authorized&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Custom SMTP isn't saved or enabled&lt;/td&gt;
&lt;td&gt;Save the SMTP settings again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;535&lt;/code&gt; / authentication failed&lt;/td&gt;
&lt;td&gt;Password isn't a valid API key&lt;/td&gt;
&lt;td&gt;Paste the full key, or create a new one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;550&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sender domain isn't verified, or the account is pending&lt;/td&gt;
&lt;td&gt;Finish domain setup in the dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;450&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Warm-up cap or 10 a second reached&lt;/td&gt;
&lt;td&gt;Wait, or lower Supabase's rate limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase: rate limit exceeded&lt;/td&gt;
&lt;td&gt;Supabase's own hourly limit&lt;/td&gt;
&lt;td&gt;Raise it under Rate Limits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After 5 failed logins from one IP address we block that address for 15 minutes, so fix a wrong password before retrying many times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Edit the templates
&lt;/h2&gt;

&lt;p&gt;Supabase writes these emails, so you change the wording under &lt;strong&gt;Authentication&lt;/strong&gt;, then &lt;strong&gt;Email Templates&lt;/strong&gt;: confirm signup, invite, magic link, change email address, reset password and reauthentication. Templates use Go template variables: put &lt;code&gt;.ConfirmationURL&lt;/code&gt; or &lt;code&gt;.Token&lt;/code&gt; inside double curly braces.&lt;/p&gt;

&lt;p&gt;Keep auth emails plain: a short line, one button, the same link as text, and no tracking or marketing. That's what mailbox providers expect from a password reset, and it's what gets it to the inbox quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option B: A Send Email Hook that calls our API
&lt;/h2&gt;

&lt;p&gt;If you want your own templates, per-user languages or a record of each send in your database, use Supabase's &lt;strong&gt;Send Email Hook&lt;/strong&gt;. When the hook is on, Supabase stops using SMTP and calls your endpoint for every auth email. You build the email and send it through &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;&lt;code&gt;POST /emails&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create an Edge Function and its secrets. The hook secret comes from &lt;strong&gt;Authentication&lt;/strong&gt;, then &lt;strong&gt;Hooks&lt;/strong&gt;, when you add a Send Email Hook pointing to the function's URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase functions new send-email
supabase secrets &lt;span class="nb"&gt;set &lt;/span&gt;&lt;span class="nv"&gt;OMS_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;... &lt;span class="nv"&gt;SEND_EMAIL_HOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"v1,whsec_..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// supabase/functions/send-email/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Webhook&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://esm.sh/standardwebhooks@1.0.0&lt;/span&gt;&lt;span class="dl"&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;hookSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SEND_EMAIL_HOOK_SECRET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v1,whsec_&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="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROJECT_REF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-project-ref&lt;/span&gt;&lt;span class="dl"&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;subjects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Confirm your email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;magiclink&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 sign-in link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;recovery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reset your password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;invite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You've been invited&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email_change&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Confirm your new email address&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reauthentication&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 confirmation code&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="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;payload&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email_data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hookSecret&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;http_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid signature&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PROJECT_REF&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.supabase.co/auth/v1/verify?`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email_action_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;redirect_to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redirect_to&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://api.ownmailserver.com/emails&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;Deno&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OMS_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&gt;// The same hook call retried won't send twice.&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`supabase-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token_hash&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;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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Acme &amp;lt;no-reply@auth.acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&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="nx"&gt;subjects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email_action_type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Message from Acme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;p&amp;gt;&amp;lt;a href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;Continue&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;Or enter this code: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Continue: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nOr enter this code: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&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;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="o"&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;ok&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;body&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;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;http_code&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="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&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="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{}&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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="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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;supabase functions deploy send-email &lt;span class="nt"&gt;--no-verify-jwt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy with &lt;code&gt;--no-verify-jwt&lt;/code&gt;: Supabase Auth signs the hook call with the hook secret, not a user's JWT, and the function checks that signature itself.&lt;/p&gt;

&lt;p&gt;Two details from Supabase's docs to handle before you rely on it. With &lt;strong&gt;Secure Email Change&lt;/strong&gt; on, an &lt;code&gt;email_change&lt;/code&gt; call carries two token pairs, one for each address, and Supabase notes the field names are reversed from what you'd expect, so follow its hook docs for that case. Newer versions of the hook can also send security notifications, such as &lt;code&gt;password_changed_notification&lt;/code&gt;; give those their own subject and text rather than the fallback above.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this isn't the right fit
&lt;/h2&gt;

&lt;p&gt;If your project only sends a handful of auth emails, any SMTP provider with a free tier will do, and Supabase's limits are the real constraint, not cost. We're a good fit when you want auth and app email from one provider on your own domain, with bounces, complaints and warm-up handled for you. The &lt;a href="https://ownmailserver.com/guides/firebase-email-setup" rel="noopener noreferrer"&gt;Firebase guide&lt;/a&gt; covers the same setup for Firebase, and &lt;a href="https://ownmailserver.com/pricing" rel="noopener noreferrer"&gt;pay-as-you-go pricing&lt;/a&gt; is $0.20 per 1,000 emails with a $10 monthly minimum.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does Supabase say "Email address not authorized"?
&lt;/h3&gt;

&lt;p&gt;Your project is still using Supabase's built-in email sender, which only delivers to members of your Supabase organization's team. Any other address gets that error. Set up custom SMTP in your project's Authentication settings and Supabase can email anyone. The built-in sender is meant for trying Supabase out, and Supabase says it has no delivery guarantee.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I raise Supabase's email rate limit?
&lt;/h3&gt;

&lt;p&gt;Set up custom SMTP first: the built-in sender is fixed at 2 emails an hour. After that, Supabase starts you at 30 emails an hour, which you can raise on the project's Rate Limits page in the Authentication settings. Raise it in steps that match your warm-up cap with us, so the extra email isn't refused during your first weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What SMTP port should I use for Supabase?
&lt;/h3&gt;

&lt;p&gt;Use 587. Supabase connects, upgrades the connection with STARTTLS and then logs in, which is what we require. Port 25 isn't for authenticated sending, and we don't offer 465. If Supabase reports a login error, the password is usually wrong: it must be a full API key from your OwnMailServer dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I change the text of Supabase auth emails?
&lt;/h3&gt;

&lt;p&gt;Yes. Supabase's Email Templates page lets you edit the subject and HTML of each email, such as confirm signup, magic link and reset password, and custom SMTP sends whatever you write there. For full control, such as your own template system or languages per user, use a Send Email Hook and build each email yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should Supabase auth emails use a separate domain?
&lt;/h3&gt;

&lt;p&gt;It helps. Supabase recommends separate sending domains or From addresses for auth and marketing email, such as auth.example.com and marketing.example.com. Mailbox providers judge each domain's reputation, so a newsletter that draws complaints won't drag your password resets into spam. With us, verify the subdomain as its own sending domain and use it as Supabase's sender address.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ownmailserver.com/guides/supabase-custom-smtp" rel="noopener noreferrer"&gt;ownmailserver.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>email</category>
      <category>auth</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to set up Auth0 custom SMTP with OwnMailServer</title>
      <dc:creator>Hani Khanani</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:30:06 +0000</pubDate>
      <link>https://dev.to/hani_khanani_2842a21e1b57/how-to-set-up-auth0-custom-smtp-with-ownmailserver-1ei4</link>
      <guid>https://dev.to/hani_khanani_2842a21e1b57/how-to-set-up-auth0-custom-smtp-with-ownmailserver-1ei4</guid>
      <description>&lt;p&gt;Auth0's built-in email sender is only for testing, so every production tenant needs its own provider. To send through OwnMailServer, open &lt;strong&gt;Branding&lt;/strong&gt;, then &lt;strong&gt;Email Provider&lt;/strong&gt;, turn on &lt;strong&gt;Use my own email provider&lt;/strong&gt;, choose &lt;strong&gt;SMTP&lt;/strong&gt;, and enter &lt;code&gt;smtp.ownmailserver.com&lt;/code&gt;, port &lt;code&gt;587&lt;/code&gt;, any username and an API key as the password, with a From address on your verified domain. Then click &lt;strong&gt;Send Test Email&lt;/strong&gt;. It takes about ten minutes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SMTP&lt;/td&gt;
&lt;td&gt;Most tenants: Auth0's templates, sent from your domain&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Provider Action + our API&lt;/td&gt;
&lt;td&gt;Your own logic per message type, retries, logging&lt;/td&gt;
&lt;td&gt;One Action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why Auth0 needs its own email provider
&lt;/h2&gt;

&lt;p&gt;Auth0 sends email for verification, password resets, verification codes, MFA codes, welcome messages, blocked-account notices and organization invitations. Out of the box it uses a built-in sender that, as of October 2026, Auth0 documents like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All emails come from &lt;code&gt;no-reply@auth0user.net&lt;/code&gt;. You can't change the From address.&lt;/li&gt;
&lt;li&gt;It allows &lt;strong&gt;10 emails a minute&lt;/strong&gt;, of every type together.&lt;/li&gt;
&lt;li&gt;You can't use custom email templates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auth0 says to use it for testing only. With your own provider, sign-in emails come from your domain, signed with your DKIM key, and every bounce and complaint shows up in your dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An approved OwnMailServer account with a &lt;strong&gt;verified sending domain&lt;/strong&gt;. The From address must be on that domain, or we refuse the email with &lt;code&gt;550&lt;/code&gt;. The &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; list the DNS records, and the &lt;a href="https://ownmailserver.com/guides/how-to-set-up-dkim" rel="noopener noreferrer"&gt;DKIM guide&lt;/a&gt; explains them.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;API key&lt;/strong&gt;, created in your dashboard under &lt;strong&gt;API keys &amp;amp; SMTP&lt;/strong&gt;. Choose &lt;strong&gt;Sending only&lt;/strong&gt; (the default), limited to your sending domain, so the key Auth0 stores can't do anything but send. Make one just for Auth0, so you can revoke it without touching your other apps.&lt;/li&gt;
&lt;li&gt;Ideally a &lt;strong&gt;separate subdomain for auth email&lt;/strong&gt;, such as &lt;code&gt;no-reply@auth.acme.com&lt;/code&gt;. Mailbox providers judge each domain's reputation, so a newsletter that draws complaints won't pull your password resets into spam.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Enter the SMTP settings
&lt;/h2&gt;

&lt;p&gt;In the Auth0 Dashboard, open &lt;strong&gt;Branding&lt;/strong&gt;, then &lt;strong&gt;Email Provider&lt;/strong&gt;. Turn on &lt;strong&gt;Use my own email provider&lt;/strong&gt;, select &lt;strong&gt;SMTP&lt;/strong&gt;, and fill in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;From&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;no-reply@auth.acme.com&lt;/code&gt; (on your verified domain)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Host&lt;/td&gt;
&lt;td&gt;&lt;code&gt;smtp.ownmailserver.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;587&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ownmailserver&lt;/code&gt; (we accept any value)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;Your API key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;Save&lt;/strong&gt;. Auth0 lists four requirements for an SMTP server, and we meet each one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Auth0 requires&lt;/th&gt;
&lt;th&gt;OwnMailServer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SMTP AUTH with the LOGIN method&lt;/td&gt;
&lt;td&gt;LOGIN and PLAIN, after STARTTLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS 1.2 or later&lt;/td&gt;
&lt;td&gt;Yes, on port 587&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A certificate from a public certificate authority&lt;/td&gt;
&lt;td&gt;Let's Encrypt, valid for &lt;code&gt;smtp.ownmailserver.com&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accept connections from Auth0's IP addresses&lt;/td&gt;
&lt;td&gt;We accept connections from anywhere and check the API key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you change the host, port or username later, Auth0 asks for the password again, so keep the API key somewhere safe, such as your secrets manager.&lt;/p&gt;

&lt;p&gt;To set the same thing from a script or CI, use Auth0's Management API with a token that has the &lt;code&gt;update:email_provider&lt;/code&gt; scope. Use &lt;code&gt;POST&lt;/code&gt; the first time and &lt;code&gt;PATCH&lt;/code&gt; to change it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; PATCH &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="nv"&gt;$AUTH0_DOMAIN&lt;/span&gt;&lt;span class="s2"&gt;/api/v2/emails/provider"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AUTH0_MGMT_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "name": "smtp",
    "enabled": true,
    "default_from_address": "no-reply@auth.acme.com",
    "credentials": {
      "smtp_host": "smtp.ownmailserver.com",
      "smtp_port": 587,
      "smtp_user": "ownmailserver",
      "smtp_pass": "'&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OMS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Send a test email
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Send Test Email&lt;/strong&gt; on the same page. Then search for the address in your OwnMailServer dashboard to see the delivery, or ask the &lt;a href="https://ownmailserver.com/claude-email-connector" rel="noopener noreferrer"&gt;Claude connector&lt;/a&gt; what happened to it. If nothing arrives after a few minutes, check &lt;strong&gt;Monitoring&lt;/strong&gt;, then &lt;strong&gt;Logs&lt;/strong&gt; in Auth0 for a failed send:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You see&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;535&lt;/code&gt; / authentication failed&lt;/td&gt;
&lt;td&gt;Password isn't a valid API key&lt;/td&gt;
&lt;td&gt;Paste the full key, or create a new one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;550&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;From domain isn't verified, or the account is pending&lt;/td&gt;
&lt;td&gt;Finish domain setup in the dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;450&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Warm-up cap or 10 a second reached&lt;/td&gt;
&lt;td&gt;Wait; Auth0 logs the failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;421&lt;/code&gt; too many failed logins&lt;/td&gt;
&lt;td&gt;5 failed logins from one IP address&lt;/td&gt;
&lt;td&gt;Fix the password, wait 15 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email still from &lt;code&gt;auth0user.net&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The provider toggle is off or not saved&lt;/td&gt;
&lt;td&gt;Turn on Use my own email provider and save&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 3: Edit the templates
&lt;/h2&gt;

&lt;p&gt;With your own provider set up, open &lt;strong&gt;Branding&lt;/strong&gt;, then &lt;strong&gt;Email Templates&lt;/strong&gt; to change each email: verification email (link and code), change password, welcome email, MFA enrollment, blocked account and others. Each template has its own &lt;strong&gt;From&lt;/strong&gt; field, which overrides the default, so keep it on your verified domain too. Templates use Liquid: put a variable such as &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;user.email&lt;/code&gt; or &lt;code&gt;application.name&lt;/code&gt; inside double curly braces.&lt;/p&gt;

&lt;p&gt;Keep auth emails plain: a short line, one button, the same link as text, and no tracking or marketing. That's what mailbox providers expect from a password reset, and it's what gets it to the inbox quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option B: A Custom Provider Action that calls our API
&lt;/h2&gt;

&lt;p&gt;If you want your own logic, such as logging each send or handling message types differently, choose &lt;strong&gt;Custom Provider&lt;/strong&gt; under &lt;strong&gt;Branding&lt;/strong&gt;, then &lt;strong&gt;Email Provider&lt;/strong&gt;. Auth0 then stops using SMTP and runs an Action on the &lt;code&gt;custom-email-provider&lt;/code&gt; trigger for every email, passing the rendered subject, HTML and text. Your Action sends them through &lt;a href="https://ownmailserver.com/docs/api" rel="noopener noreferrer"&gt;&lt;code&gt;POST /emails&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Enter the &lt;strong&gt;From&lt;/strong&gt; address, add your API key as a secret named &lt;code&gt;OMS_API_KEY&lt;/code&gt; (the key icon in the editor), and use this code:&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="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onExecuteCustomEmailProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&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://api.ownmailserver.com/emails&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OMS_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;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&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;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&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="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tags&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;auth0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message_type&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Network error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&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="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="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;ok&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;text&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// 429 (rate or daily cap) and 5xx are temporary: Auth0 retries up to 5 times.&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;429&lt;/span&gt; &lt;span class="o"&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;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OwnMailServer &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="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;body&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="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`OwnMailServer &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="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;body&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;Save to deploy it, then click &lt;strong&gt;Send Test Email&lt;/strong&gt;. Errors such as an unverified domain (&lt;code&gt;4xx&lt;/code&gt; other than &lt;code&gt;429&lt;/code&gt;) won't fix themselves, so the Action drops them and Auth0 logs the reason instead of retrying. The &lt;code&gt;tags&lt;/code&gt; value records Auth0's message type with each email, such as &lt;code&gt;reset_email&lt;/code&gt; or &lt;code&gt;verify_email&lt;/code&gt;, so you can tell them apart later.&lt;/p&gt;

&lt;p&gt;An Action is a tenant-wide setting: only one Action can be bound to the &lt;code&gt;custom-email-provider&lt;/code&gt; trigger. If you manage Auth0 with Terraform, the &lt;code&gt;auth0_email_provider&lt;/code&gt; resource with &lt;code&gt;name = "custom"&lt;/code&gt; does the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep auth email within your warm-up cap
&lt;/h2&gt;

&lt;p&gt;A new OwnMailServer account starts at a few hundred recipients a day and rises about 25% after each clean day (&lt;a href="https://ownmailserver.com/guides/email-ip-warm-up" rel="noopener noreferrer"&gt;how warm-up works&lt;/a&gt;). Your dashboard shows today's cap. Verification and reset emails rarely come close. If you're moving an existing tenant with many daily signups, keep the old provider meanwhile and switch Auth0 over once the cap covers a normal day, as described in our &lt;a href="https://ownmailserver.com/email-migration" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this isn't the right fit
&lt;/h2&gt;

&lt;p&gt;If your tenant only sends a handful of auth emails, any SMTP provider with a free tier will do, and Auth0's 10-a-minute test sender is the real problem, not cost. We're a good fit when you want auth and app email from one provider on your own domain, with bounces, complaints and warm-up handled for you. The &lt;a href="https://ownmailserver.com/guides/supabase-custom-smtp" rel="noopener noreferrer"&gt;Supabase guide&lt;/a&gt; and &lt;a href="https://ownmailserver.com/guides/firebase-email-setup" rel="noopener noreferrer"&gt;Firebase guide&lt;/a&gt; cover the same setup for those platforms, and &lt;a href="https://ownmailserver.com/pricing" rel="noopener noreferrer"&gt;pay-as-you-go pricing&lt;/a&gt; is $0.20 per 1,000 emails with a $10 monthly minimum.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why do my Auth0 emails come from &lt;a href="mailto:no-reply@auth0user.net"&gt;no-reply@auth0user.net&lt;/a&gt;?
&lt;/h3&gt;

&lt;p&gt;Your tenant still uses Auth0's built-in email provider. It always sends from &lt;a href="mailto:no-reply@auth0user.net"&gt;no-reply@auth0user.net&lt;/a&gt;, allows 10 emails a minute and doesn't let you use custom templates. Auth0 says it is for testing only. Turn on Use my own email provider under Branding, Email Provider, set up SMTP or another provider, and your emails come from your own domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  What SMTP port should I use for Auth0?
&lt;/h3&gt;

&lt;p&gt;Use 587. Auth0 recommends it because it's the current standard port for sending and supports TLS, and it's the port we offer: Auth0 connects, upgrades the connection with STARTTLS and logs in with the LOGIN method. We don't offer 465, and port 25 isn't for authenticated sending. If Auth0 reports a login failure, the password must be a full OwnMailServer API key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Auth0 need my SMTP server to allow its IP addresses?
&lt;/h3&gt;

&lt;p&gt;Auth0 requires your SMTP server to accept connections from Auth0's IP addresses, and lists them under Branding, Email Provider. Our SMTP server accepts connections from anywhere and checks the API key instead, so you don't need to add anything. A firewall on your side only matters if you run your own mail server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I send Auth0 emails through an API instead of SMTP?
&lt;/h3&gt;

&lt;p&gt;Yes. Choose Custom Provider under Branding, Email Provider and write an Action for the custom-email-provider trigger. Auth0 passes it the rendered subject, HTML and text, and your code sends them with our POST /emails endpoint. The Action can ask Auth0 to retry up to 5 times when we answer with a temporary error.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many Auth0 emails can I send on a new OwnMailServer account?
&lt;/h3&gt;

&lt;p&gt;A new account starts with a daily cap of a few hundred recipients, and the cap rises about 25% after each clean day. Auth email such as verification and password resets is usually low volume, so most apps stay well under it. If you also send a large welcome email to every new signup, keep that in mind while the cap grows.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://ownmailserver.com/guides/auth0-smtp-setup" rel="noopener noreferrer"&gt;ownmailserver.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>auth0</category>
      <category>email</category>
      <category>auth</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
