<?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: Chitransh Gupta</title>
    <description>The latest articles on DEV Community by Chitransh Gupta (@cheeto).</description>
    <link>https://dev.to/cheeto</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3804749%2Fe5dc64e5-bbe8-4df4-a997-7bf23285864e.jpg</url>
      <title>DEV Community: Chitransh Gupta</title>
      <link>https://dev.to/cheeto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cheeto"/>
    <language>en</language>
    <item>
      <title>Email as the Human-in-the-Loop for AI Agents</title>
      <dc:creator>Chitransh Gupta</dc:creator>
      <pubDate>Thu, 19 Mar 2026 10:24:26 +0000</pubDate>
      <link>https://dev.to/cheeto/email-as-the-human-in-the-loop-for-ai-agents-12k3</link>
      <guid>https://dev.to/cheeto/email-as-the-human-in-the-loop-for-ai-agents-12k3</guid>
      <description>&lt;p&gt;In July 2025, a developer told his AI agent eleven times, in ALL CAPS, not to touch production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.indykite.ai/blogs/when-good-intentions-go-rogue-the-risk-of-ai-agents#:~:text=AI%20agents%20show%20up%20ready,isn't%20an%20edge%20case." rel="noopener noreferrer"&gt;It deleted the database anyway.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Replit agent wiped months of work, fabricated data to cover its tracks, and when asked to rate its own handling of the situation, gave itself a 95/100 on the data catastrophe scale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F865lua7la114147u10ix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F865lua7la114147u10ix.png" alt="Reddit thread of Replit agent going rogue" width="800" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This isn't isolated. Developers across the community are &lt;a href="https://www.reddit.com/r/AI_Agents/comments/1qd0mjs" rel="noopener noreferrer"&gt;actively asking&lt;/a&gt; how to stop agents from going rogue in production.&lt;/p&gt;

&lt;p&gt;The answer isn't always better instructions. Sometimes it's a simple checkpoint: &lt;strong&gt;ask a human first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One way to do this: A human-in-the-loop approval system built using email. The agent pauses, sends an approval email, and waits. One tap from your inbox and it knows whether to proceed or not. No new tools. Just your inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scenario
&lt;/h2&gt;

&lt;p&gt;There's an agent monitoring support tickets. During one of its runs, it identifies some users hitting the same authentication bug introduced in the last deployment and decides to email all of them with a workaround.&lt;/p&gt;

&lt;p&gt;Sounds helpful. But what if the workaround is wrong? What if it emails the wrong users? Without any guardrails, it would just do it.&lt;/p&gt;

&lt;p&gt;Instead, it pauses and sends you an approval email with just enough context to make the call:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt; Send workaround email to 12 affected users&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug:&lt;/strong&gt; Authentication failure on login after v2.3.1 deployment&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sample affected users:&lt;/strong&gt; &lt;a href="mailto:john@acme.com"&gt;john@acme.com&lt;/a&gt;, &lt;a href="mailto:sara@corp.io"&gt;sara@corp.io&lt;/a&gt;, &lt;a href="mailto:mike@startup.com"&gt;mike@startup.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message that will be sent:&lt;/strong&gt; &lt;em&gt;"We've identified an issue affecting your account after our recent update. As a temporary workaround, please clear your browser cache and log in again."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You spot-check a name or two, and tap &lt;strong&gt;Approve&lt;/strong&gt; or &lt;strong&gt;Reject&lt;/strong&gt; right from your inbox. Nothing happens until you decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Here's the full flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent detects users hitting the same bug
        ↓
Server generates two signed URLs (approve + reject)
        ↓
Resend sends an approval email with context and two buttons to the approver
        ↓
Approver taps Approve or Reject from their inbox
        ↓
Next.js API route verifies the signature and checks expiry
        ↓
Decision is recorded, agent proceeds or stops
        ↓
Confirmation email fires, loop closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three moving parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The agent&lt;/strong&gt; → detects the issue and triggers an approval request instead of acting immediately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The approval email&lt;/strong&gt; → built with &lt;a href="https://react.email/" rel="noopener noreferrer"&gt;React Email&lt;/a&gt;, sent via Resend, contains two buttons backed by signed URLs. This is the only thing the approver ever sees.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Next.js API routes&lt;/strong&gt; → generate the signed URLs, send the email, handle the click, and update the agent's state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://nodejs.org/en/download" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;a href="https://resend.com" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; account with a &lt;a href="https://resend.com/docs/dashboard/domains/introduction#verifying-a-domain" rel="noopener noreferrer"&gt;verified domain&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;a href="https://groq.com" rel="noopener noreferrer"&gt;Groq&lt;/a&gt; account for a free API key&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting Up the Project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Calm-Rock/agent-email-approval.git
&lt;span class="nb"&gt;cd &lt;/span&gt;agent-email-approval
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;.env.local&lt;/code&gt; file in the root by using &lt;code&gt;.env.example&lt;/code&gt; as a reference:&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;re_xxxxxxxxx
&lt;span class="nv"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_key                &lt;span class="c"&gt;# generate with: openssl rand -hex 32&lt;/span&gt;
&lt;span class="nv"&gt;APPROVAL_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3000
&lt;span class="nv"&gt;APPROVER_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@youremail.com
&lt;span class="nv"&gt;FROM_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;agent@yourdomain.com
&lt;span class="nv"&gt;GROQ_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_groq_api_key_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;RESEND_API_KEY&lt;/code&gt; → Your Resend &lt;a href="https://resend.com/docs/dashboard/api-keys/introduction#add-api-key" rel="noopener noreferrer"&gt;API key&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;SECRET_KEY&lt;/code&gt; → A random secret string used to sign the approve and reject URLs. This makes sure nobody can forge a click. You can generate one by running &lt;code&gt;openssl rand -hex 32&lt;/code&gt; in your terminal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;APPROVAL_BASE_URL&lt;/code&gt; → The base URL of your Next.js server. During development this will be &lt;code&gt;http://localhost:3000&lt;/code&gt;. When you deploy, replace this with your actual domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;APPROVER_EMAIL&lt;/code&gt; → The email address that will receive the approval request. It can be your personal Gmail, work email, anything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;FROM_EMAIL&lt;/code&gt; → The email address the agent sends from. This must be a &lt;a href="https://resend.com/docs/dashboard/domains/introduction#verifying-a-domain" rel="noopener noreferrer"&gt;verified domain&lt;/a&gt; on your Resend account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;GROQ_API_KEY&lt;/code&gt; → Your Groq API key for ticket analysis. Get a free key &lt;a href="https://console.groq.com/keys" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how the project is laid out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent-email-approval/
├── emails/
│   └── ApprovalEmail.jsx
├── app/
│   └── api/
│       ├── request-approval/
│       │   └── route.jsx
│       ├── approval/
│       │   └── route.js
│       └── approval-status/
│           └── route.js
├── utils/
│   ├── tokens.js
│   └── store.js
├── agent.js
├── tickets.json
└── .env.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are covered in the sections that follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Approval Email
&lt;/h2&gt;

&lt;p&gt;The approval email is built with &lt;a href="https://react.email/" rel="noopener noreferrer"&gt;React Email&lt;/a&gt;. It carries just enough context for the approver to make an informed decision: the bug, how many users are affected, a sample of who's impacted, and the exact message that will be sent to them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhoiaosc15n6b3pjgkqmr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhoiaosc15n6b3pjgkqmr.png" alt="Approval email template with approve and reject options." width="800" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two buttons, Approve and Reject, are backed by signed URLs.&lt;/p&gt;

&lt;p&gt;But why buttons? Why not just let the approver reply with 'APPROVE' or 'REJECT'?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;One tap and you're done. No typing, no formatting, no opening a reply window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reply parsing is fragile and error-prone. Email clients add signatures, quote previous messages, and format text differently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replies can be spoofed. A signed URL is cryptographically tied to the action and expires after 24 hours.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The signed URL is the authentication. The approver doesn't need to log in anywhere. Clicking the button is the proof they have the right to decide.&lt;/p&gt;

&lt;p&gt;You can find the full React Email template code at &lt;a href="https://github.com/Calm-Rock/agent-email-approval/blob/main/emails/ApprovalEmail.jsx" rel="noopener noreferrer"&gt;emails/ApprovalEmail.jsx&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Approval Flow
&lt;/h2&gt;

&lt;p&gt;This is where everything connects. When the agent detects an issue, it hits &lt;code&gt;/api/request-approval&lt;/code&gt;. That endpoint generates two signed URLs, one to approve and one to reject, and fires off the approval email via Resend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfw3snth5bmudsimx11m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmfw3snth5bmudsimx11m.png" alt="Agent approval flow diagram." width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Signing the URLs
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/Calm-Rock/agent-email-approval/blob/main/utils/tokens.js" rel="noopener noreferrer"&gt;utils/tokens.js&lt;/a&gt; handles this. Each URL contains the action ID, the decision, and an expiry timestamp, all signed with HMAC-SHA256 using your &lt;code&gt;SECRET_KEY&lt;/code&gt;. Verification uses &lt;code&gt;timingSafeEqual&lt;/code&gt; instead of a regular string comparison to prevent timing attacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending the Email
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/Calm-Rock/agent-email-approval/blob/main/app/api/request-approval/route.jsx" rel="noopener noreferrer"&gt;app/api/request-approval/route.jsx&lt;/a&gt; takes the action details, generates the signed URLs, and sends the approval email via Resend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling the Click
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/Calm-Rock/agent-email-approval/blob/main/app/api/approval/route.js" rel="noopener noreferrer"&gt;app/api/approval/route.js&lt;/a&gt; does four things in order: verifies the signature, checks expiry, guards against double clicks, and records the decision. Once recorded, it fires a confirmation email back to the approver and returns a response the approver sees in their browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Agent
&lt;/h2&gt;

&lt;p&gt;The agent is a standalone Node.js script, &lt;code&gt;agent.js&lt;/code&gt;, that runs separately from the Next.js server. It communicates with the server entirely over HTTP.&lt;/p&gt;

&lt;p&gt;Here's what it does in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Loads tickets from &lt;code&gt;tickets.json&lt;/code&gt;, a set of sample support tickets used to simulate real incoming requests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sends them to Groq for analysis to determine which users are affected, what the bug is, and what workaround message to send&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hits &lt;code&gt;/api/request-approval&lt;/code&gt; with the analysis and waits&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Polls &lt;code&gt;/api/approval-status&lt;/code&gt; every 3 seconds for a decision. The agent has no way of knowing when the approver has clicked, so it keeps checking the server until it gets an answer. In production, you could flip this around and have the server notify the agent directly via a webhook once the decision is recorded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If approved, sends the workaround email to every affected user via Resend&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If rejected, stops&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To run it, start the Next.js server first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in a separate terminal run the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node agent.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;NOTE: In Resend's free plan, you are limited to 2 requests per second. The agent already includes a 600ms delay between sends to handle this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's what it looks like when you run it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F629f27b7aa1e065bd75f6b54%2F8b3de551-f110-4ad5-9130-3b9b8dfb3a52.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F629f27b7aa1e065bd75f6b54%2F8b3de551-f110-4ad5-9130-3b9b8dfb3a52.gif" alt="Approval email flow from agent to inbox" width="760" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the agent finishes, you can check your Resend dashboard to see the delivery statuses for each email.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fodoeczxagrxgw6xdd8jn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fodoeczxagrxgw6xdd8jn.png" alt="Resend dashboard showing email status" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the full agent script at &lt;a href="https://github.com/Calm-Rock/agent-email-approval/blob/main/agent.js" rel="noopener noreferrer"&gt;agent.js&lt;/a&gt; and the sample tickets at &lt;a href="https://github.com/Calm-Rock/agent-email-approval/blob/main/tickets.json" rel="noopener noreferrer"&gt;tickets.json&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Protects Against
&lt;/h2&gt;

&lt;p&gt;This setup doesn't make your agent smarter. It makes its mistakes catchable before they cause damage. Specifically, three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incomplete reasoning:&lt;/strong&gt; The agent decided on a workaround, but it doesn't know if it's correct, worded right, or targeting the right users. You do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Irreversible actions:&lt;/strong&gt; Emails can't be unsent. A ten second checkpoint is cheaper than a mass email apology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forged or replayed clicks:&lt;/strong&gt; The token is tied to a specific action ID, expires after 24 hours, and can only be used once. A forged click gets rejected.&lt;/p&gt;

&lt;p&gt;One limitation worth knowing: this is only as secure as the approver's inbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Take It Next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Replace the in-memory store with a database:&lt;/strong&gt; Decisions reset on every server restart. Swap &lt;a href="https://github.com/Calm-Rock/agent-email-approval/blob/main/utils/store.js" rel="noopener noreferrer"&gt;utils/store.js&lt;/a&gt; for a proper database and you get persistence and an audit trail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-stakeholder approvals:&lt;/strong&gt; Require two out of three approvers before the agent proceeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeout handling:&lt;/strong&gt; Add a timeout so the agent stops or escalates if no decision is made within X hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhook instead of polling:&lt;/strong&gt; Have the server notify the agent directly once a decision is recorded, instead of polling every 3 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversational approvals:&lt;/strong&gt; Resend just launched a &lt;a href="https://resend.com/docs/chat-sdk" rel="noopener noreferrer"&gt;Chat SDK&lt;/a&gt; that turns email into a two-way channel. The approver could ask follow-up questions before making a final call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The Replit agent deleted a production database, fabricated data to cover its tracks, and rated its own handling a 95 out of 100 on the catastrophe scale. Not because it was told the wrong thing. Because nothing stopped it from acting before a human could intervene.&lt;/p&gt;

&lt;p&gt;That's the gap this fills. Not smarter instructions, not better prompts. Just a checkpoint before the action runs. One email, two buttons, the agent waits for a human to decide.&lt;/p&gt;

&lt;p&gt;Email isn't the only way to put a human in the loop. But it's about as universal as it gets.&lt;/p&gt;

&lt;p&gt;The full code is available on &lt;a href="https://github.com/Calm-Rock/agent-email-approval" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>resend</category>
      <category>agents</category>
      <category>react</category>
      <category>ai</category>
    </item>
    <item>
      <title>Email Is Infrastructure. Start Treating It Like One.</title>
      <dc:creator>Chitransh Gupta</dc:creator>
      <pubDate>Thu, 19 Mar 2026 10:04:48 +0000</pubDate>
      <link>https://dev.to/cheeto/email-is-infrastructure-start-treating-it-like-one-14fp</link>
      <guid>https://dev.to/cheeto/email-is-infrastructure-start-treating-it-like-one-14fp</guid>
      <description>&lt;p&gt;For most people, email is a UI experience. You open Gmail or Yahoo, click Compose, write a subject and body, add a recipient, and hit send.&lt;/p&gt;

&lt;p&gt;From an application perspective, email behaves more like infrastructure. Messages pass through external providers, delivery happens asynchronously, and a single send can trigger multiple downstream events.&lt;/p&gt;

&lt;p&gt;But if email is infrastructure, can it be made observable like the rest of an application?&lt;/p&gt;

&lt;p&gt;Yes. By converting email lifecycle events into telemetry.&lt;/p&gt;

&lt;p&gt;This guide shows how to do that. Using &lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; webhooks and &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; (OTel), email events (sent, delivered, bounced, complained) become traces and metrics visible alongside the rest of your application telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Resend fires a &lt;a href="https://resend.com/docs/webhooks/introduction#what-is-a-webhook" rel="noopener noreferrer"&gt;webhook&lt;/a&gt; for every email event. A lightweight webhook receiver picks it up and emits a OpenTelemetry span and metric. This guide uses SigNoz as the observability backend, but the same setup should work with any &lt;a href="https://opentelemetry.io/ecosystem/vendors/" rel="noopener noreferrer"&gt;OTLP compatible vendor&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your App → Resend API
                ↓
         Resend sends email
                ↓
         Email event occurs
         (delivered / bounced / complained)
                ↓
         Resend fires webhook
                ↓
         Webhook receiver (index.js)
         emits spans + metrics
                ↓
         SigNoz / OTel Vendor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Interesting insight 💡&lt;br&gt;&lt;br&gt;
Because this approach stores Resend webhook events as traces and metrics in the observability backend, you get &lt;a href="https://resend.com/docs/dashboard/webhooks/how-to-store-webhooks-data" rel="noopener noreferrer"&gt;webhook retention&lt;/a&gt; as part of your observability data for free. On SigNoz Cloud that is 15 days for traces and 30 days for metrics. On self-hosted SigNoz you can also move older data to &lt;a href="https://signoz.io/docs/userguide/retention-period/" rel="noopener noreferrer"&gt;cold storage&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; account and &lt;a href="https://resend.com/api-keys" rel="noopener noreferrer"&gt;API key&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; v18+ installed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://signoz.io/teams/" rel="noopener noreferrer"&gt;SigNoz Cloud&lt;/a&gt; account (self-hosted works too)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt; to expose your local webhook endpoint&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Sample App
&lt;/h2&gt;

&lt;p&gt;The sample app is a Node.js app that simulates a user registration flow, one of the most common transactional email use cases. When a user registers, the app sends a welcome email built with &lt;a href="https://react.email/" rel="noopener noreferrer"&gt;React Email&lt;/a&gt; via Resend.&lt;/p&gt;

&lt;p&gt;You can find the full code in the &lt;a href="https://github.com/Calm-Rock/resend-email-observability" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;. Clone it and install dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Calm-Rock/resend-email-observability.git
&lt;span class="nb"&gt;cd &lt;/span&gt;resend-email-observability
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OTel packages required for tracing and metrics are already included in &lt;code&gt;package.json&lt;/code&gt; and will be installed automatically.&lt;/p&gt;

&lt;p&gt;
  What OTel packages are installed?
  &lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@opentelemetry/api" rel="noopener noreferrer"&gt;@opentelemetry/api&lt;/a&gt; - the core OpenTelemetry API for creating spans and metrics&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node" rel="noopener noreferrer"&gt;@opentelemetry/auto-instrumentations-node&lt;/a&gt; - automatically instruments HTTP, Express and other libraries without any code changes. This package also pulls in the Node.js SDK and OTLP exporters as dependencies, so you don't need to install them separately.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;p&gt;The sample app (&lt;code&gt;sample-app.js&lt;/code&gt;) has three endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;POST /register&lt;/code&gt; → sends a welcome email to a real address&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;POST /register/bounce&lt;/code&gt; → triggers a bounce scenario&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;POST /register/spam&lt;/code&gt; → triggers a spam complaint scenario&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&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;cp .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And add your Resend API key (starts with &lt;code&gt;re_&lt;/code&gt;) and sender email.&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;your_api_key
&lt;span class="nv"&gt;SENDER_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;yourdomain.com&lt;/code&gt; should be a domain you have &lt;a href="https://resend.com/docs/dashboard/domains/introduction#verifying-a-domain" rel="noopener noreferrer"&gt;verified in Resend&lt;/a&gt;. If you don’t have a verified domain yet, you can use &lt;code&gt;onboarding@resend.dev&lt;/code&gt; as the sender email for testing. However, emails sent from this address can only be delivered to the &lt;strong&gt;email address associated with your Resend account&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Run the sample app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node sample-app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will start the sample app on port 3001.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send a test email&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Send a test email to verify the sample app is working:&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; POST http://localhost:3001/register &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": "Your Name", "email": "your@email.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;your@email.com&lt;/code&gt; with the email address where you want to receive the test welcome email. You will receive the welcome email as shown below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fir9ae3xuit6cjpb8wva2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fir9ae3xuit6cjpb8wva2.png" alt="Welcome email from Sample App"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Webhook Receiver
&lt;/h2&gt;

&lt;p&gt;The webhook receiver is a single Express app (&lt;code&gt;index.js&lt;/code&gt;) that listens for &lt;a href="https://resend.com/docs/webhooks/introduction" rel="noopener noreferrer"&gt;Resend webhook&lt;/a&gt; events and converts them into spans and metrics.&lt;/p&gt;

&lt;p&gt;When an email is sent through Resend, webhook events are triggered for each state change in the email lifecycle. Each payload contains a &lt;code&gt;type&lt;/code&gt; field describing the event, such as &lt;code&gt;email.sent&lt;/code&gt;, &lt;code&gt;email.delivered&lt;/code&gt;, or &lt;code&gt;email.bounced&lt;/code&gt;, and a &lt;code&gt;data&lt;/code&gt; object with details about the email. See the &lt;a href="https://resend.com/docs/webhooks/event-types" rel="noopener noreferrer"&gt;full list of event types&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here is an example &lt;code&gt;email.sent&lt;/code&gt; payload:&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;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-05T20:18:44.871Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-05T20:18:44.739Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"095e18f0-ef44-4327-a083-a2ec5a19a27c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"onboarding@resend.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome to Forge — your workspace is ready"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="s2"&gt;"test@gmail.com"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email.sent"&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;h3&gt;
  
  
  Setting up the webhook endpoint
&lt;/h3&gt;

&lt;p&gt;The webhook receiver listens for &lt;code&gt;POST&lt;/code&gt; requests on &lt;code&gt;/webhook&lt;/code&gt;. When a Resend event arrives, it extracts the email details from the payload, creates an OTel span and increments a metric counter.&lt;/p&gt;

&lt;p&gt;The complete code for the webhook receiver is in the &lt;a href="https://github.com/Calm-Rock/resend-email-observability/blob/main/index.js" rel="noopener noreferrer"&gt;index.js&lt;/a&gt; file in the resend-email-observability repository you cloned earlier.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ NOTE&lt;br&gt;&lt;br&gt;
For simplicity this example does not verify webhook signatures. In production environments you should verify &lt;a href="https://resend.com/docs/webhooks/verify-webhooks-requests" rel="noopener noreferrer"&gt;Resend webhook signatures&lt;/a&gt; to ensure requests originate from Resend and prevent spoofed events.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is a walkthrough of the key sections:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parsing the webhook payload&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each incoming webhook request is parsed to extract the event type and relevant email details.&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="nx"&gt;event&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="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventType&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;type&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;data&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailId&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toEmail&lt;/span&gt; &lt;span class="o"&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;to&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="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&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;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractDomain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toEmail&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;bounceType&lt;/span&gt; &lt;span class="o"&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="kc"&gt;null&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;bounceSubType&lt;/span&gt; &lt;span class="o"&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;subType&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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;eventType&lt;/code&gt; is the event name like &lt;code&gt;email.bounced&lt;/code&gt;. &lt;code&gt;bounceType&lt;/code&gt; and &lt;code&gt;bounceSubType&lt;/code&gt; are only present in bounce events and contain values like &lt;code&gt;Permanent&lt;/code&gt; or &lt;code&gt;Transient&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initializing the tracer and meter&lt;/strong&gt;&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="nx"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resend-webhook-receiver&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;meter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMeter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resend-webhook-receiver&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;emailEventCounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCounter&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.events&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;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Count of Resend email events by type&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;This sets up the OTel tracer for creating spans and a counter metric that will track email events by type.&lt;/p&gt;

&lt;p&gt;Traces let you debug individual emails, while metrics power aggregate dashboards. The webhook receiver emits both for every event.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating a span for each email event&lt;/strong&gt;&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="nx"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`resend.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventType&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attributes&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;email.event_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventType&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.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;emailId&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.to&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toEmail&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.domain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domain&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.from&lt;/span&gt;&lt;span class="dl"&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="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;unknown&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;email.subject&lt;/span&gt;&lt;span class="dl"&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;subject&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&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;email.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;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bounceType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;attributes&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.bounce_type&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;bounceType&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;bounceSubType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;attributes&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.bounce_subtype&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;bounceSubType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each webhook event becomes a span with a name like &lt;code&gt;resend.email.bounced&lt;/code&gt;. Email details are attached as attributes such as &lt;code&gt;email.id&lt;/code&gt;, &lt;code&gt;email.to&lt;/code&gt;, &lt;code&gt;email.domain&lt;/code&gt;, &lt;code&gt;email.subject&lt;/code&gt;, along with event specific attributes like &lt;code&gt;email.bounce_type&lt;/code&gt; for bounce events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incrementing the metric counter&lt;/strong&gt;&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="nx"&gt;metricAttributes&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;email.event_type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventType&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.domain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domain&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;bounceType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;metricAttributes&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.bounce_type&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;bounceType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;emailEventCounter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;metricAttributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SpanStatusCode&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="nx"&gt;span&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="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;200&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;received&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every event increments the &lt;code&gt;email.events&lt;/code&gt; counter tagged with the event type and recipient domain as dimensions. Bounce events also include &lt;code&gt;email.bounce_type&lt;/code&gt; as a metric attribute, so you can track hard and soft bounces separately on your dashboard. After the metric is recorded, &lt;code&gt;span.setStatus&lt;/code&gt; marks the span as successful and &lt;code&gt;span.end()&lt;/code&gt; closes it. This is important because spans are only exported after they complete.&lt;/p&gt;

&lt;p&gt;Returning a &lt;code&gt;200&lt;/code&gt; response is important because Resend uses it to confirm the webhook was received. If your endpoint returns a non-2xx status, Resend will retry delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exposing webhook receiver with ngrok
&lt;/h3&gt;

&lt;p&gt;Resend needs a public URL to deliver webhook events to. During local development, you can expose your server using ngrok:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ngrok http 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook receiver runs on port &lt;code&gt;3000&lt;/code&gt; . The ngrok command above exposes it as a public HTTPS URL that Resend can deliver webhooks to.&lt;/p&gt;

&lt;p&gt;Copy the &lt;strong&gt;HTTPS forwarding URL&lt;/strong&gt; from the ngrok output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forwarding  https://your-id.ngrok-free.app -&amp;gt; http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;https://your-id.ngrok-free.app&lt;/code&gt; is your forwarding URL. Append &lt;code&gt;/webhook&lt;/code&gt; to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://your-id.ngrok-free.app/webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure the endpoint URL ends with &lt;code&gt;/webhook&lt;/code&gt;, since that is the route the receiver listens on.&lt;/p&gt;

&lt;p&gt;This is the URL you will add to Resend in the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Resend webhooks
&lt;/h3&gt;

&lt;p&gt;Go to the Resend &lt;a href="https://resend.com/webhooks" rel="noopener noreferrer"&gt;Webhooks dashboard&lt;/a&gt; and click &lt;strong&gt;Add Webhook&lt;/strong&gt;. Paste your ngrok webhook URL (with &lt;code&gt;/webhook&lt;/code&gt;) and select &lt;strong&gt;All Events&lt;/strong&gt; so your endpoint receives every webhook event.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forasdbvv6ugnrvtu6p8h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Forasdbvv6ugnrvtu6p8h.png" alt="Webhook configuration in Rese"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running the Webhook Receiver
&lt;/h2&gt;

&lt;p&gt;Start the webhook receiver with OpenTelemetry configured to export to SigNoz. Run the below commands in the root folder:&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;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_TRACES_EXPORTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;otlp
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-ingestion-endpoint&amp;gt;:443
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"signoz-ingestion-key=&amp;lt;your-ingestion-key&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_TIMEOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5000
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_NODE_RESOURCE_DETECTORS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;env&lt;/span&gt;,host,os
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;resend-email-observability
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--require @opentelemetry/auto-instrumentations-node/register"&lt;/span&gt; 
node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
  Click to know details about the OTEL environment variables
  &lt;p&gt;These environment variables configure the OTel SDK without any code changes. OTEL_EXPORTER_OTLP_ENDPOINT tells the SDK where to send data, OTEL_SERVICE_NAME is how your service will appear in SigNoz, and NODE_OPTIONS loads the &lt;a href="https://opentelemetry.io/docs/zero-code/js/" rel="noopener noreferrer"&gt;auto-instrumentation&lt;/a&gt; library which automatically captures HTTP spans alongside your custom email spans.&lt;/p&gt;

&lt;p&gt;Detailed explanation about the OTel environment variables can be found in the OpenTelemetry docs for &lt;a href="https://opentelemetry.io/docs/languages/sdk-configuration/general/" rel="noopener noreferrer"&gt;SDK configuration&lt;/a&gt; and &lt;a href="https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/" rel="noopener noreferrer"&gt;OTLP exporter configuration&lt;/a&gt;.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;&amp;lt;your-ingestion-key&amp;gt;&lt;/code&gt; with your SigNoz &lt;a href="https://signoz.io/docs/ingestion/signoz-cloud/keys/#add-an-ingestion-key" rel="noopener noreferrer"&gt;ingestion key&lt;/a&gt; and &lt;code&gt;&amp;lt;your-ingestion-endpoint&amp;gt;&lt;/code&gt; with your SigNoz &lt;a href="https://signoz.io/docs/ingestion/signoz-cloud/overview/#endpoint" rel="noopener noreferrer"&gt;ingestion endpoint&lt;/a&gt;, For example, &lt;code&gt;https://ingest.us.signoz.cloud&lt;/code&gt; .&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: For Self-Hosted version of SigNoz, update the endpoint and remove the ingestion key header as shown &lt;a href="https://signoz.io/docs/ingestion/cloud-vs-self-hosted/#cloud-to-self-hosted" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a separate terminal, start the sample application by running this command in the root folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node sample-app.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Triggering email events
&lt;/h3&gt;

&lt;p&gt;The sample application has three endpoints, each simulating a different email delivery outcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/register&lt;/code&gt; → triggers &lt;code&gt;email.sent&lt;/code&gt; and &lt;code&gt;email.delivered&lt;/code&gt; events&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/register/bounce&lt;/code&gt; → triggers &lt;code&gt;email.sent&lt;/code&gt; and &lt;code&gt;email.bounced&lt;/code&gt; events&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/register/spam&lt;/code&gt; → triggers &lt;code&gt;email.sent&lt;/code&gt; and &lt;code&gt;email.complained&lt;/code&gt; events&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trigger all three scenarios:&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; POST http://localhost:3001/register &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": "Test User", "email": "your@email.com"}'&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3001/register/bounce &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": "Test User"}'&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3001/register/spam &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": "Test User"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;your@email.com&lt;/code&gt; with your own email address in the first command.&lt;/p&gt;

&lt;p&gt;You should see webhook events arriving in your terminal:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbxd3ni7bvu15r5bmg08.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flbxd3ni7bvu15r5bmg08.png" alt="Webhook Events Terminal Output"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These events are now being converted into OTel spans and metrics. Let's see them in SigNoz.&lt;/p&gt;

&lt;h2&gt;
  
  
  Viewing Traces in SigNoz
&lt;/h2&gt;

&lt;p&gt;Head over to SigNoz and click on &lt;strong&gt;Traces&lt;/strong&gt;. Filter by &lt;code&gt;service.name = resend-email-observability&lt;/code&gt; and you should see a trace for the different webhook events.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqig503r26cp2xw9dv0my.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqig503r26cp2xw9dv0my.png" alt="Webhook events as trace in SigNoz"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on any trace to see the full span details. Here is what a &lt;code&gt;resend.email.bounced&lt;/code&gt; span looks like:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptenucq5lxytvusjpea1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptenucq5lxytvusjpea1.png" alt="Span detail of bounced email event"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trace shows a &lt;code&gt;POST /webhook&lt;/code&gt; request flowing through the JSON parser middleware into the webhook handler, which creates the &lt;code&gt;resend.email.bounced&lt;/code&gt; child span with all email attributes attached.&lt;/p&gt;

&lt;p&gt;Every email attribute attached in &lt;code&gt;index.js&lt;/code&gt; is visible here — &lt;code&gt;email.id&lt;/code&gt;, &lt;code&gt;email.to&lt;/code&gt;, &lt;code&gt;email.domain&lt;/code&gt;, &lt;code&gt;email.bounce_type&lt;/code&gt; and more. This means you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Filter traces by &lt;code&gt;email.bounce_type = Permanent&lt;/code&gt; to find all hard bounces&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Filter by &lt;code&gt;email.domain&lt;/code&gt; to see which domains are causing issues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Filter by &lt;code&gt;email.id&lt;/code&gt; to trace the full lifecycle of a single email across multiple events&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Custom Dashboard
&lt;/h2&gt;

&lt;p&gt;Traces help you debug individual emails. A dashboard shows the aggregate picture.&lt;/p&gt;

&lt;p&gt;A ready-to-import SigNoz dashboard JSON is available &lt;a href="https://github.com/Calm-Rock/resend-email-observability/blob/main/dashboards/resend-email-observability.json" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Import it from &lt;strong&gt;Dashboards → New Dashboard → Import JSON&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The dashboard includes panels for email event volume over time, distribution by event type, bounce type breakdown, events by recipient domain, and HTTP endpoint latency and status codes from auto-instrumentation.&lt;/p&gt;

&lt;p&gt;Each panel includes a description. Hover over the ⓘ icon in your Dashboard to read it.&lt;/p&gt;

&lt;p&gt;Here is what the dashboard looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8gjdexu4i55oh00qagws.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8gjdexu4i55oh00qagws.png" alt="Ready to use Email Observability Dashboard."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing worth highlighting is that SigNoz dashboards are &lt;a href="https://signoz.io/docs/dashboards/interactivity/" rel="noopener noreferrer"&gt;interactive&lt;/a&gt;. Clicking any data point in a panel lets you jump directly to the related traces, so you can move from aggregate metrics to individual email spans in a single click.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyof0m1glxxq1gczonjl9.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyof0m1glxxq1gczonjl9.gif" alt="Jumping from metrics to spans"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;If events are not appearing as expected, use this table to diagnose the issue.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely 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;No data in SigNoz, no errors in terminal&lt;/td&gt;
&lt;td&gt;Wrong ingestion endpoint or key&lt;/td&gt;
&lt;td&gt;Double-check both values against your &lt;a href="https://signoz.io/docs/ingestion/signoz-cloud/overview/" rel="noopener noreferrer"&gt;SigNoz ingestion settings&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webhook events log in terminal but no traces in SigNoz&lt;/td&gt;
&lt;td&gt;Incorrect &lt;code&gt;OTEL_SERVICE_NAME&lt;/code&gt; or endpoint&lt;/td&gt;
&lt;td&gt;Verify all &lt;code&gt;export&lt;/code&gt; commands ran in the same terminal session before &lt;code&gt;node index.js&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events worked before, now nothing arrives&lt;/td&gt;
&lt;td&gt;ngrok URL changed after a session restart&lt;/td&gt;
&lt;td&gt;Update the webhook URL in the &lt;a href="https://resend.com/webhooks" rel="noopener noreferrer"&gt;Resend Webhooks dashboard&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email sends but no webhook event logged in terminal&lt;/td&gt;
&lt;td&gt;Resend webhook not configured or pointed at wrong URL&lt;/td&gt;
&lt;td&gt;Check the Resend Webhooks dashboard for delivery attempts and errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;email.delivered&lt;/code&gt; never arrives&lt;/td&gt;
&lt;td&gt;Delivery confirmation can take several minutes&lt;/td&gt;
&lt;td&gt;Wait for some time, or check Resend logs for delivery status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;email.bounced&lt;/code&gt; or &lt;code&gt;email.complained&lt;/code&gt; not triggering&lt;/td&gt;
&lt;td&gt;Test endpoints use hardcoded Resend test addresses&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;/register/bounce&lt;/code&gt; and &lt;code&gt;/register/spam&lt;/code&gt; as-is — no email field needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Alerting on bounce spikes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;SigNoz lets you set up &lt;a href="https://signoz.io/docs/alerts-management/metrics-based-alerts/" rel="noopener noreferrer"&gt;alerts on any metric&lt;/a&gt;. A useful one is alerting when the bounce rate crosses a threshold.&lt;/p&gt;

&lt;p&gt;For example, if &lt;code&gt;email.bounced&lt;/code&gt; events exceed a set threshold in a 5 minute window, you can configure this from &lt;strong&gt;Alerts → New Alert → Metrics Alert&lt;/strong&gt; in SigNoz using the &lt;code&gt;email.events&lt;/code&gt; metric filtered by &lt;code&gt;email.event_type = 'email.bounced'&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Using the Docker image&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you'd rather not install Node.js, you can run the webhook receiver as a Docker container using a &lt;a href="https://hub.docker.com/r/chitranshgupta/resend-webhook-receiver" rel="noopener noreferrer"&gt;pre-built Docker image&lt;/a&gt; . A &lt;code&gt;Dockerfile&lt;/code&gt; is also included in the &lt;a href="https://github.com/Calm-Rock/resend-email-observability" rel="noopener noreferrer"&gt;repository&lt;/a&gt; if you want to customize it.&lt;/p&gt;

&lt;p&gt;Run the pre-built image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-ingestion-endpoint&amp;gt;:443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"signoz-ingestion-key=&amp;lt;your-ingestion-key&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_TIMEOUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;resend-email-observability &lt;span class="se"&gt;\&lt;/span&gt;
  chitranshgupta/resend-webhook-receiver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;your-ingestion-key&amp;gt;&lt;/code&gt; with your SigNoz &lt;a href="https://signoz.io/docs/ingestion/signoz-cloud/keys/#add-an-ingestion-key" rel="noopener noreferrer"&gt;ingestion key&lt;/a&gt; , &lt;code&gt;&amp;lt;your-ingestion-endpoint&amp;gt;&lt;/code&gt; with your SigNoz &lt;a href="https://signoz.io/docs/ingestion/signoz-cloud/overview/#endpoint" rel="noopener noreferrer"&gt;ingestion endpoint&lt;/a&gt;. For example, &lt;code&gt;https://ingest.us.signoz.cloud&lt;/code&gt; .&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Most teams find out about email delivery problems when users complain. With Resend webhooks and OpenTelemetry, you find out the moment it happens in the same place you already watch the rest of your stack.&lt;/p&gt;

</description>
      <category>resend</category>
      <category>infrastructure</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The macOS Terminal Email Setup Nobody Talks About</title>
      <dc:creator>Chitransh Gupta</dc:creator>
      <pubDate>Tue, 03 Mar 2026 23:29:31 +0000</pubDate>
      <link>https://dev.to/cheeto/the-macos-terminal-email-setup-nobody-talks-about-5682</link>
      <guid>https://dev.to/cheeto/the-macos-terminal-email-setup-nobody-talks-about-5682</guid>
      <description>&lt;p&gt;A while ago, while watching a video about "&lt;a href="https://www.youtube.com/watch?v=VhxybXQWEdM" rel="noopener noreferrer"&gt;How Email Works?&lt;/a&gt;" I got to know about UNIX mail and how you can send email from your terminal to anyone with a single command. I set it up with Gmail, went through 2FA, App Passwords, and a handful of extra steps, and finally got to excitedly send a test email to a friend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cdn.imgchest.com/files/15642212a2df.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpkg619wwds3osktd0yhc.png" alt="Email from my terminal to my friend."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting there was some work, and I wanted one thing: my own domain in that sender address instead of @&lt;a href="http://gmail.com" rel="noopener noreferrer"&gt;gmail.com&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
While looking around, I remembered &lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; where I had set up my domain and it had been refreshingly simple. A quick check confirmed they have their own SMTP service that uses an API key to send emails from your verified domain. Perfect!&lt;/p&gt;

&lt;p&gt;This guide will show you how to set up your macOS terminal to send emails using Resend. Once it's configured, you can send a quick note from a script, get updates from a long-running build, or have an AI agent ping you when a task completes, all without leaving your shell.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://resend.com/signup" rel="noopener noreferrer"&gt;Resend Account&lt;/a&gt; (it's free)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://resend.com/docs/dashboard/api-keys/introduction#add-api-key" rel="noopener noreferrer"&gt;API key&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://resend.com/docs/dashboard/domains/introduction#verifying-a-domain" rel="noopener noreferrer"&gt;Verified Domain&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prefer a one-command setup?&lt;/strong&gt; Run the &lt;a href="https://gist.github.com/Calm-Rock/6409106dd48edf07dd65db4405a2337f" rel="noopener noreferrer"&gt;setup script&lt;/a&gt; directly. It handles everything interactively and sends a test email at the end. Otherwise, follow the guide below to understand exactly what is happening under the hood.&lt;/p&gt;

&lt;p&gt;Download the script, unzip it, and run:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x resend-macos-setup.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./resend-macos-setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;macOS comes pre-built with Postfix, the default &lt;a href="https://en.wikipedia.org/wiki/Message_transfer_agent" rel="noopener noreferrer"&gt;Mail Transfer Agent (MTA)&lt;/a&gt; which works on-demand. Whenever you send an email from your terminal, it is put in a local queue and Postfix tries to authenticate with your SMTP server. Once the authentication is successful, it relays the message from the queue to Resend, which then handles delivery to the destination.&lt;/p&gt;

&lt;p&gt;Let's configure Postfix to send messages to your Resend SMTP server.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: &lt;strong&gt;Configure Postfix as a Relay Host&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A relayhost is an intermediate mail server that receives your local emails and relays them to their destination. In this case, you will be using Resend as your SMTP provider.&lt;/p&gt;

&lt;p&gt;Run the following command in your terminal:&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;sudo &lt;/span&gt;nano /etc/postfix/main.cf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will open the Postfix config file. Scroll to the bottom of the file and add the following lines to point Postfix to Resend's SMTP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mydestination =
relayhost = [smtp.resend.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
smtp_sasl_mechanism_filter = login
smtp_generic_maps = hash:/etc/postfix/generic
local_transport = error:local delivery disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
  🔧 View Configuration Details
  &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;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;relayhost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sends all mail through Resend's SMTP server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smtp_sasl_auth_enable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smtp_sasl_password_maps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Points to the file with your Resend API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smtp_sasl_mechanism_filter = login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ensures Postfix uses the LOGIN handshake required by Resend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smtp_tls_security_level = encrypt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mandates encryption to protect your API key during transit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smtp_generic_maps&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maps your local Mac username (eg., &lt;code&gt;cheeto@MacBook.local&lt;/code&gt;) to a verified domain email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;local_transport = error: ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prevents Postfix from trying local delivery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mydestination =&lt;/code&gt; (empty)&lt;/td&gt;
&lt;td&gt;Forces Postfix to treat every email as external and send it to relay&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;/p&gt;

&lt;p&gt;By default, your Mac tries to send mail as &lt;code&gt;localuser@hostname.local&lt;/code&gt;, which modern servers like Resend will block. So, we use &lt;a href="http://postfix.org/generic.5.html" rel="noopener noreferrer"&gt;generic maps&lt;/a&gt; to rewrite that local address to your verified domain. The &lt;code&gt;local_transport&lt;/code&gt; and &lt;code&gt;mydestination&lt;/code&gt; settings act as a one-way street sign. Without them, Postfix would try to deliver your email locally to your Mac, silently succeeding at "delivery" while your message goes nowhere. These two settings together tell Postfix that every message is outbound.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Secure Your Authentication Credentials
&lt;/h3&gt;

&lt;p&gt;To allow Postfix to log in to Resend, you need to create a credential file and compile it into a format the system can read quickly. Run the following commands in your terminal:&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;sudo &lt;/span&gt;bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo "[smtp.resend.com]:587 resend:YOUR_RESEND_API_KEY" &amp;gt; /etc/postfix/sasl_passwd'&lt;/span&gt;

&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/postfix/sasl_passwd

&lt;span class="nb"&gt;sudo &lt;/span&gt;postmap /etc/postfix/sasl_passwd

&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/postfix/sasl_passwd.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Replace &lt;code&gt;YOUR_RESEND_API_KEY&lt;/code&gt; with the full key from your Resend dashboard (it usually starts with &lt;code&gt;re_&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  ⚠️ Do this now — Clear your shell history
  &lt;p&gt;The command above writes your API key inline, which means it will be saved to your shell's history file (&lt;code&gt;.zsh_history&lt;/code&gt; or &lt;code&gt;.bash_history&lt;/code&gt;). After running it, clear the entry:&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="c"&gt;# zsh&lt;/span&gt;
&lt;span class="nb"&gt;fc&lt;/span&gt; &lt;span class="nt"&gt;-p&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="c"&gt;# bash&lt;/span&gt;
&lt;span class="nb"&gt;history&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;history &lt;/span&gt;1&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;postmap&lt;/code&gt; compiles your credentials into a fast lookup database that Postfix can read. The &lt;code&gt;chmod 600&lt;/code&gt; step is not optional. Without it, your API key sits in a plain-text file readable by any process on your machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Map Your Local User to a Verified Domain
&lt;/h3&gt;

&lt;p&gt;By default, your Mac tries to send mail from &lt;code&gt;username@hostname&lt;/code&gt;. Resend will reject this non-verified address, so you must use a generic map to rewrite your local identity to your real, verified email address. Run the following commands in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo bash -c 'echo "$(whoami)@$(hostname) SENDER@YOURDOMAIN.COM" &amp;gt; /etc/postfix/generic'

sudo postmap /etc/postfix/generic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Replace &lt;code&gt;SENDER@YOURDOMAIN.COM&lt;/code&gt;with your verified Resend email. For example, &lt;code&gt;cheeto@cheeto.dev&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  🛠️ Having issues? Verify your hostname
  &lt;p&gt;Postfix uses its own internal &lt;code&gt;$myhostname&lt;/code&gt; to match addresses against the generic map. If it differs from what &lt;code&gt;$(hostname)&lt;/code&gt; returns in your shell, the map never fires, Resend rejects the address, the queue empties, and nothing is delivered with zero error messages.&lt;/p&gt;

&lt;p&gt;Run this to check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postconf myhostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should match &lt;code&gt;$(whoami)@$(hostname)&lt;/code&gt; exactly. If it doesn't, replace the &lt;code&gt;$(hostname)&lt;/code&gt; part of the command above with the exact output of &lt;code&gt;postconf myhostname&lt;/code&gt; and re-run it.&lt;/p&gt;



&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;This creates a mapping file to replace your Mac's local identity (&lt;code&gt;$(whoami)@$(hostname)&lt;/code&gt;) with your verified domain. Running &lt;code&gt;postmap&lt;/code&gt; then compiles this into a &lt;code&gt;.db&lt;/code&gt; binary that Postfix can actually read and query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Start/Reload Postfix
&lt;/h3&gt;

&lt;p&gt;Postfix on macOS is "on-demand," meaning it doesn't run as a persistent background service. It spins up when needed. Run this to start it and load your new configuration in one go:&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;sudo &lt;/span&gt;postfix start&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;postfix reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Postfix was already running, the &lt;code&gt;start&lt;/code&gt; command will say so. That is not an error, just ignore it. The &lt;code&gt;reload&lt;/code&gt; is what picks up your config changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing your Configuration
&lt;/h2&gt;

&lt;p&gt;Send a test email with the command below:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello from my Mac Terminal via Resend"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Postfix Test with Resend"&lt;/span&gt; recipient@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Replace &lt;code&gt;recipient@example.com&lt;/code&gt; with the actual email address where you want to receive the message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Confirming Success
&lt;/h3&gt;

&lt;p&gt;Run this to check the local queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mailq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it returns &lt;code&gt;Mail queue is empty&lt;/code&gt;, Postfix has handed the message off. That does not mean it was delivered yet. Head to the &lt;strong&gt;Emails section of your Resend Dashboard&lt;/strong&gt; to confirm the actual delivery status.&lt;/p&gt;

&lt;p&gt;
  🛠️ Having issues?
  &lt;p&gt;Open a second terminal tab and run:&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;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/mail.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a live view of exactly what Postfix is doing when you send. If something is failing, the error will show up here in real time.&lt;/p&gt;



&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cdn.imgchest.com/files/127d89593a40.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd9xa4aesb0sz4jxrwpna.png" alt="Resend Dashboard showing email successfully delivered."&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Alternative: A Faster Setup
&lt;/h2&gt;

&lt;p&gt;If Postfix feels like overkill, &lt;a href="https://marlam.de/msmtp/" rel="noopener noreferrer"&gt;msmtp&lt;/a&gt; is a lightweight, relay-only client that is much easier to configure. It doesn't run in the background and simply sends your mail directly to Resend when you call it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;msmtp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create and Configure the &lt;code&gt;.msmtprc&lt;/code&gt; File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano ~/.msmtprc &lt;span class="c"&gt;#No sudo needed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the following into the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defaults
auth           on
tls            on          # STARTTLS on port 587 for encrypted connection
tls_starttls   on
logfile        ~/.msmtp.log

account resend
host smtp.resend.com
port 587
from SENDER@YOURDOMAIN.COM
user resend
password YOUR_RESEND_API_KEY

account default : resend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Replace &lt;code&gt;YOUR_RESEND_API_KEY&lt;/code&gt; with the full key from your Resend dashboard and &lt;code&gt;SENDER@YOURDOMAIN.COM&lt;/code&gt; with your verified Resend email.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Secure Permissions
&lt;/h3&gt;

&lt;p&gt;This file contains your plain-text API key. You must lock it down to owner-only access or msmtp will refuse to run for security reasons:&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;chmod &lt;/span&gt;600 ~/.msmtprc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send a Test Email
&lt;/h3&gt;

&lt;p&gt;Verify that your setup is working properly by sending a test email using the command below:&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;printf&lt;/span&gt; &lt;span class="s2"&gt;"Subject: msmtp Test via Resend&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;To: recipient@example.com&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Hello from msmtp via Resend."&lt;/span&gt; | msmtp recipient@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Replace &lt;code&gt;recipient@example.com&lt;/code&gt; with the actual email address where you want to receive the message.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unlike Postfix, msmtp gives you instant feedback directly in your terminal. If the connection to Resend fails, you will know immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use: AI Agent Notifications
&lt;/h2&gt;

&lt;p&gt;If you're running Claude Code, Codex, or any local agent loop, you can now give your agent the ability to send email with a single shell command:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"Task complete: dependency audit finished. 3 issues found."&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Agent Report: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; SENDER@YOURDOMAIN.COM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this as a final step in any agent workflow. No Slack bot, no webhook, no extra API to set up. Just your terminal, talking back to you.&lt;/p&gt;

</description>
      <category>resend</category>
      <category>cli</category>
      <category>agents</category>
      <category>smtp</category>
    </item>
  </channel>
</rss>
