Get Started
Start HereBlogGuidesResourcesPricingFAQAbout Get Started
📧 Resend Advanced

Advanced Resend: Templates, Webhooks, Bounce Handling, and Deliverability

Build reusable email templates with React Email, handle bounces and complaints automatically, and protect your domain reputation long-term.

The basics guide got you to a working email flow. This guide covers what you need to run that flow reliably at scale — templates that render consistently across email clients, webhook handling for bounces and complaints, and the deliverability fundamentals that protect your domain reputation over time.

If you skip the bounce and complaint handling, your sender reputation will eventually degrade and your emails will start landing in spam. This isn’t hypothetical. It happens gradually, then all at once.


React Email: Building Templates That Actually Render

HTML email is one of the most hostile rendering environments in software. Outlook still uses Microsoft Word’s rendering engine in some versions. Gmail strips certain CSS properties. Apple Mail renders things differently than iOS Mail. What looks fine in one client looks broken in another.

React Email is Resend’s open-source library for building email templates as React components. You write clean, readable JSX. React Email compiles it to battle-tested HTML that’s been tuned to render consistently across the major email clients. You get to write modern code; your customers get emails that look right in Outlook 2019.

Setting up React Email locally

You need Node.js installed. In your project directory:

npm install react-email @react-email/components

Create a folder for your templates — emails/ is conventional. Inside it, create a file for your first template, job-confirmation.tsx:

import {
  Html,
  Head,
  Body,
  Container,
  Section,
  Text,
  Heading,
  Hr,
  Preview,
} from '@react-email/components';

interface JobConfirmationProps {
  customerName: string;
  jobType: string;
  appointmentDate: string;
  technicianName: string;
}

export default function JobConfirmation({
  customerName,
  jobType,
  appointmentDate,
  technicianName,
}: JobConfirmationProps) {
  return (
    <Html>
      <Head />
      <Preview>Your {jobType} appointment is confirmed</Preview>
      <Body style={{ fontFamily: 'Arial, sans-serif', backgroundColor: '#f9f9f9' }}>
        <Container style={{ maxWidth: '600px', margin: '0 auto', backgroundColor: '#ffffff', padding: '32px' }}>
          <Heading style={{ fontSize: '24px', color: '#1a1a1a' }}>
            Appointment Confirmed
          </Heading>
          <Text style={{ color: '#444', fontSize: '16px' }}>
            Hi {customerName},
          </Text>
          <Text style={{ color: '#444', fontSize: '16px' }}>
            Your {jobType} appointment is confirmed. Here are the details:
          </Text>
          <Section style={{ backgroundColor: '#f4f4f4', padding: '16px', borderRadius: '4px' }}>
            <Text style={{ margin: 0, color: '#1a1a1a' }}><strong>Date:</strong> {appointmentDate}</Text>
            <Text style={{ margin: '8px 0 0', color: '#1a1a1a' }}><strong>Technician:</strong> {technicianName}</Text>
          </Section>
          <Hr style={{ borderColor: '#e5e5e5', margin: '24px 0' }} />
          <Text style={{ color: '#888', fontSize: '14px' }}>
            Questions? Reply to this email and we'll get back to you.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Rendering to HTML and using in n8n

To use this in an n8n workflow, you have two options:

  1. Pre-render to a static HTML string — Run npx react-email export to compile your templates to static HTML files. Copy the output HTML and use it as a template string in your n8n Code node, with placeholder values you swap out using JavaScript’s .replace() or template literals.

  2. Call a rendering endpoint — Deploy a small Node.js function (Supabase Edge Functions work well here) that accepts template variables and returns the rendered HTML. Your n8n workflow calls this endpoint, gets back the HTML, then passes it to Resend.

Option 1 is simpler to start with. Option 2 is cleaner at scale when you have multiple templates updating frequently.


Resend Webhooks: Knowing What Happens After You Send

Sending an email is only half the story. You need to know what happened to it. Did it arrive? Did it bounce? Did someone mark it as spam? Resend’s webhook system gives you this information by sending a POST request to your n8n webhook URL whenever a tracked event occurs.

Setting up the webhook in Resend

Go to Resend Dashboard → Webhooks → Add Endpoint. Enter the URL of your n8n webhook trigger node. Select which events you want to receive. Start with these four:

  • email.delivered — Receiving server accepted the message
  • email.bounced — Delivery permanently failed
  • email.complained — Recipient marked as spam
  • email.opened — Recipient opened the email

Save the webhook. Resend will show you the signing secret — copy this. You’ll need it to verify that incoming requests actually came from Resend.

Building the n8n webhook handler

Create a new n8n workflow with a Webhook trigger. Set it to respond to POST requests. Copy the webhook URL into Resend.

Add a Switch node after the webhook trigger to branch based on {{ $json.type }}:

  • email.bounced → bounce handling branch
  • email.complained → complaint handling branch
  • email.delivered → logging branch (optional)

Each branch handles its event type independently. I’ll cover the bounce and complaint branches in detail below.


Handling Bounces: Hard vs Soft

A bounce means your email couldn’t be delivered. But not all bounces are the same.

Hard bounce — Permanent delivery failure. The email address doesn’t exist, the domain doesn’t accept email, or the receiving server has permanently rejected your address. Common causes: typos in the email address, domains that no longer exist, old addresses that have been deactivated.

Soft bounce — Temporary delivery failure. The email address is real but something temporary prevented delivery: the recipient’s inbox is full, the receiving mail server is temporarily unavailable, the message was too large.

Resend’s bounce webhook payload includes a data.bounce.type field with values like hard or soft.

Hard bounce handling in n8n:

  1. Webhook receives email.bounced event with bounce_type: "hard"
  2. Extract the recipient email address from data.to[0]
  3. HTTP Request → Supabase REST API → find the customer record matching that email
  4. Update the customer record: email_bounced = true, email_bounced_at = now()
  5. Add a log entry to a bounce_log table with the Resend message ID and timestamp
  6. Optionally: if this customer has an active job, notify the dispatcher that customer email is invalid

Once email_bounced = true, every n8n workflow that sends email should check this flag before sending. A simple IF node at the start of any email-sending flow: “IF customer.email_bounced is true → skip email step → continue.”

Soft bounce handling:

Soft bounces don’t require immediate action. Resend typically retries soft bounces automatically. What you should do: log the soft bounce in Supabase with a retry count. If you see more than 3 soft bounces to the same address over 7 days, treat it like a hard bounce — the address is likely unreachable.


Handling Complaints

A complaint happens when a recipient clicks “Mark as Spam” in their email client. ISPs that participate in feedback loop programs (Yahoo, Outlook/Hotmail) notify Resend, which fires the email.complained event to your webhook.

Complaints are more serious than bounces from a deliverability standpoint. Email providers monitor complaint rates by sender domain. If your complaint rate exceeds 0.1% — one complaint per thousand emails sent — you’ll start seeing deliverability problems. Above 0.5% and you risk being blacklisted.

Complaint handling in n8n:

  1. Webhook receives email.complained event
  2. Extract the recipient email from data.to[0]
  3. HTTP Request → Supabase → find the customer record
  4. Update: email_complained = true, email_opted_out = true, complaint_at = now()
  5. Log to a complaint_log table
  6. Send yourself (the operator) a notification via a separate Resend call or Twilio SMS — you want to know when complaints happen

Once email_complained = true, never email that person again. Not for jobs, not for receipts, not for anything. The only exception might be a legal notice, which is a different situation entirely.


Domain Reputation and Deliverability Best Practices

This section is the unglamorous foundation that everything else depends on. Email deliverability isn’t set-and-forget — it’s an ongoing hygiene practice.

Warm up new domains gradually. When you start sending email from a new domain, don’t immediately send 1,000 emails on day one. Email providers track sending history. A domain with no history suddenly sending at high volume looks like a spammer. Start with 50-100 emails per day for the first week, increase gradually over 2-4 weeks.

Maintain consistent sending volume. Erratic sending patterns — silence for two weeks, then a spike of 500 emails, then silence again — trigger spam filters. Consistent, predictable volume builds a positive sending history.

Keep bounce rates below 2%. Persistently high bounce rates signal that you’re sending to unverified or low-quality addresses. Clean your list, honor your bounces, and never buy email lists.

Keep complaint rates below 0.1%. If you’re seeing complaints, look at what you’re sending and why people are opting out this way instead of through a normal unsubscribe flow. Usually it means your email content is surprising people — they don’t recognize you, the send frequency is too high, or the content doesn’t match what they expected.

Use a subdomain for transactional email. Instead of sending from yourcompany.com, consider send.yourcompany.com or mail.yourcompany.com. This protects your main domain’s reputation — if your transactional email stream ever has a deliverability problem, it doesn’t contaminate your primary domain, which is used for your website and marketing.

Monitor with DMARC reports. Once you have DMARC configured, you’ll start receiving aggregate reports from major email providers about authentication results for your domain. These reports are dense XML, but services like Postmark’s free DMARC analyzer or dmarcian parse them into readable summaries. Look at them quarterly to catch any authentication problems early.


Email Preferences and Unsubscribe Handling

Every email that isn’t purely transactional — receipts and confirmations are fine — should give recipients a way to opt out of that type of communication. This isn’t just a legal consideration (CAN-SPAM requires it for commercial messages); it’s also how you keep complaint rates low.

Build a simple unsubscribe flow:

  1. Add an unsubscribe link to your email templates: <a href="https://yourcompany.com/unsubscribe?email={{ customerEmail }}&type=job_updates">Unsubscribe from job updates</a>
  2. Create a Supabase Edge Function or n8n webhook that receives the GET request when someone clicks that link
  3. The handler updates the customer record in Supabase: email_preferences.job_updates = false
  4. Returns a simple confirmation page: “You’ve been unsubscribed.”
  5. Every n8n flow that sends that type of email checks the preference flag before sending

Don’t rely on Resend’s built-in unsubscribe feature for automation workflows. It’s useful for broadcast emails, but for automations, you want the preference data in your own Supabase database where your workflows can query it.


Scheduling and Batching

Not all transactional emails need to go out the moment a trigger fires. Some are better scheduled.

Time-delayed emails with n8n: A common pattern is a follow-up email sent 24 hours after a job is completed. Build it with a Schedule Trigger node in n8n: run at 8am daily → query Supabase for jobs completed yesterday → loop over results → send follow-up email via Resend for each one.

Batch sending with Loop Over Items: When you need to send emails to a list of people — say, everyone whose annual service contract is up for renewal — use n8n’s Loop Over Items node to process them one at a time. Add a Wait node inside the loop (500ms between sends) to pace your sending and stay comfortably inside rate limits.

Resend’s rate limits: The free tier has a 100 emails/day limit. Pro has no daily limit but does have API rate limits (requests per second). For most automation use cases, you won’t hit these. For bulk sends, the loop-with-delay approach keeps you safe.


Multi-Domain Setup

When your automation stack grows, you’ll want to send from different addresses for different contexts. In SendJob, there are three distinct sending contexts, each using a different domain and API key:

  • jobs@sendjob.com — Job confirmations, dispatch notifications, assignment emails. High operational importance, low marketing content.
  • billing@sendjob.com — Payment receipts, invoice emails. Legal expectation of delivery.
  • hello@sendjob.com — Onboarding sequences, product updates. Lower operational criticality.

Each gets its own verified domain in Resend and its own API key. Why:

Reputation isolation. If the marketing stream (hello@) gets complaints because you misjudged your audience, it doesn’t affect the deliverability of your billing or job operations streams. Their reputations are independent.

Clear audit trails. When you look at your Resend email log filtered by domain, you immediately see which part of your operation each email belongs to.

Access scoping. If you ever work with a contractor who needs to manage email templates, you can give them a scoped API key for a specific domain without touching your billing or operations credentials.

Setting this up is straightforward: add each subdomain in Resend → Domains, add the DNS records, create a separate API key for each, and create separate n8n credentials for each key. Any n8n workflow that handles billing selects the billing credential; job workflows use the operations credential.


You’ve covered the communication stack — SMS and email. Now let’s add the revenue layer. What Is Stripe? →

This is subscriber-only content

Get full access to every Basics, Advanced, and Walkthrough guide — plus monthly deep-dives, templates, and video walkthroughs.

Already a subscriber? Sign in →