All Articles
CategoryBackend
Reading Time
15 min read
Published
2026-09-16
Word Count
3,650words

Grab a coffee — this one is a deep dive!

Transactional Email Setup with Resend and SendGrid

Summary

We answer the transactional email Resend SendGrid setup question for a mobile backend with the official docs: SPF/DKIM/DMARC, Node.js send code, webhooks, and Apple MPP's effect on open rate.

  • Resend recommends a subdomain while SendGrid verifies on the root domain; both require the verification CNAME to be left proxy-free (DNS only) in Cloudflare.
  • Resend's Node SDK needs Node ≥20, SendGrid's @sendgrid/mail needs Node ≥12; API calls go through resend.emails.send and sgMail.send.
  • A webhook is required for bounce/complaint tracking; SendGrid officially warns against putting PII in the webhook payload and tells you to watch for 429s and X-RateLimit headers on rate limits.
  • Apple Mail Privacy Protection makes open rate an unreliable signal on devices where it's enabled; trust click rate when making decisions.
Transactional Email Setup with Resend and SendGrid

The moment your mobile backend has to send a signup verification code, a password reset link, or an order notification, you run into the transactional email Resend SendGrid setup question: which service, which DNS record, which code. This post walks through the full setup — from domain verification to webhooks — based directly on both services' official documentation, shows the same verification-code email sent through both, and covers delivery quality with real sources.

💡 Pro Tip: Keep the CNAME record you use for domain verification set to "DNS only" (gray cloud) in Cloudflare — verification never completes while the orange proxy is on.

Table of Contents

Transactional vs. Marketing Email and Mobile Backend Flows

Resend's own documentation draws a clear line between the two send types: transactional email is "personalized, event-driven communication," while a marketing campaign is a bulk "Broadcast distributed to a list of people." The typical flow you'll see in a mobile backend looks like this:

  • Signup verification: the user signs up with email/phone → the backend generates a verification code → a transactional API call fires → the code arrives by email.
  • Password reset: the user taps "forgot password" → the backend generates a one-time token → an email is sent → verification happens via the link/code.
  • Order/event notification: a payment or status change fires in the backend → a templated email goes out immediately.

What these three flows share: the user expects the email right now, and any delay or landing in spam directly breaks the experience. That's why transactional sending is handled with its own reputation and infrastructure, separate from marketing newsletters.

Domain Verification: SPF, DKIM, DMARC

These three DNS records give the receiving server the guarantee that "I really manage this domain and I really sent this message." Twilio SendGrid's official docs state that domain authentication proves four things: "You own the domain... You permitted the sending email server... You verified the identity of the email sender... You validated that no one tampered with the email message in transit." In other words: domain ownership, sending permission, sender identity, and message-integrity-in-transit — four separate proofs.

If automated security is turned on, Twilio manages these records for you: "If you turn on automated security, Twilio creates and maintains SPF, DKIM, and DMARC records on your behalf." On the Resend side, verification is generally fast per the official docs: "your domain will often verify within 15 minutes of adding the DNS records. However, DNS changes can occasionally take up to 72 hours to propagate globally."

If you run your site through Cloudflare, there's one practical point to watch: Resend's docs explicitly warn — "make sure not to use proxying features (e.g., Cloudflare's orange cloud) for that record, as it will prevent verification from completing." So when you add the verification CNAME, you need to leave it as DNS only (gray cloud); otherwise verification never finishes. This is a step you run into directly on this site's own Cloudflare-based infrastructure too.

Resend also recommends using a subdomain instead of the root domain: "We strongly recommend sending emails from a subdomain (e.g., notifications.example.com) instead of your root domain." That way your transactional traffic's reputation stays separate from your corporate domain's overall reputation.

Resend Setup

Per Resend's documentation, there are two prerequisites before you start: "Your own domain, verified with Resend" and "A Resend API key." The setup steps:

  1. Add a domain. Resend supports adding a domain through four different paths: Dashboard, Resend API, Resend CLI, Resend MCP server.
  2. Choose a subdomain (e.g. notifications.example.com) and enter the recommended DNS records.
  3. Verify the DNS — leave it without proxying (gray cloud), and it usually completes within 15 minutes.
  4. Create an API key and add it to your backend's environment variables.
  5. Send with the Node SDK. The current resend package requires Node ≥20 (npm registry: "engines": {"node": ">=20"}, MIT licensed).

A simplified version of the official example (the async/await wrapper and console log removed, using apiKey as the variable name to match this post's general naming):

ts
1import { Resend } from "resend";
2 
3const resend = new Resend(apiKey);
4 
5const { data, error } = await resend.emails.send({
6 from: "Acme <[email protected]>",
8 subject: "Hello World",
9 html: "<strong>It works!</strong>",
10});

According to the API reference, there are limits to watch for in the emails.send call: the to field accepts a maximum of 50 addresses, attachments are capped at "40MB per email after Base64 encoding," and the tag value can't exceed 256 characters.

SendGrid Setup

On the SendGrid side the flow is a bit more enterprise: open an account, complete Sender Identity or Domain Authentication, generate an API key, install the @sendgrid/mail package. The official docs note that Single Sender Verification is "recommended for testing only," but Domain Authentication is required for production.

The API's behavior is fixed: "The host for Web API v3 requests is always https://api.sendgrid.com/v3/" and "An API Key must be included in the Authorization header." There's also a hard limit on total message size: "The total message size should not exceed 20MB. This includes the message itself, headers, and the combined size of any attachments."

The current @sendgrid/mail package (8.1.6 on the npm registry) requires Node ≥12 — more compatible with older infrastructure than Resend's Node ≥20 requirement, which can be a practical difference when you're migrating an older backend.

The free trial quota is stated clearly in the official docs (as of this post's publish date, 2026-09-16): "you can start a free trial that allows you to send up to 100 emails per day for 60 days." This figure can change depending on SendGrid's own plans, so confirm it on the current page before you set anything up.

bash
1npm install @sendgrid/mail

After installation you should store the API key in an environment variable; you'll see the full send code in the next section.

The Same Verification-Code Email, in Both Services' Code

This is the email you'll need most often in a mobile signup flow: sending a user a 6-digit verification code. Here are two code blocks that do the same job, using each service's official syntax:

With Resend:

ts
1import { Resend } from "resend";
2 
3const resend = new Resend(apiKey);
4 
5const { data, error } = await resend.emails.send({
6 from: "Acme <[email protected]>",
8 subject: "Your Verification Code",
9 html: `<strong>Your code: 482913</strong>`,
10});
11 
12if (error) {
13 console.error(error);
14}

With SendGrid (close to the official Node.js quickstart example):

js
1const sgMail = require("@sendgrid/mail");
2sgMail.setApiKey(apiKey); // SendGrid key read from process.env
3const msg = {
6 subject: "Sending with SendGrid is Fun",
7 text: "and easy to do anywhere, even with Node.js",
8 html: "<strong>and easy to do anywhere, even with Node.js</strong>",
9};
10sgMail
11 .send(msg)
12 .then(() => {
13 console.log("Email sent");
14 })
15 .catch((error) => {
16 console.error(error);
17 });

The logic is the same in both examples: set the API key, define recipient/sender/subject/body, wrap the send call in then/catch or try/catch. The difference is that Resend returns a single { data, error } object, while SendGrid uses a promise-based .then().catch() chain.

Delivery Quality: What Bounce, Complaint, and Open Rate Actually Mean

Sending an email isn't enough — you need to know whether it actually arrived, whether it was rejected, and whether the user complained. Both services require a webhook for this.

SendGrid's Event Webhook produces three categories of events: "Delivery events that indicate the status of email delivery... Engagement events that indicate how the recipient is interacting... Account change events that indicate changes and impacts to your account." The same doc also carries a critical privacy warning: "Never place PII in this field... Twilio employees could see these values. These values get stored long-term even if you leave the Twilio SendGrid platform." In other words, don't embed personal data like the user's name or email content into the webhook payload.

Resend's webhook event types include bounce and complaint directly: email.bounced, email.complained, email.delivered, email.delivery_delayed, email.opened, email.clicked, email.failed, email.sent, email.suppressed.

Event category
SendGrid
Resend
Delivery
delivery events (delivered, bounce, dropped, etc.)
email.delivered, email.bounced, email.delivery_delayed
Engagement
engagement events (open, click)
email.opened, email.clicked
Complaint
complaint included under delivery/engagement
email.complained
Account
account change events
domain.*, contact.*

When you hit a rate limit, SendGrid returns a 429: "When you reach a rate limit, you can no longer make requests against that endpoint for the remainder of the refresh period, and the API returns a 429 response." The response headers carry information like X-RateLimit-Limit: 150 and X-RateLimit-Remaining: 0 — read these headers when you build retry logic in your backend.

There's a critical catch with open rate: Apple Mail Privacy Protection. Apple's own guide states: "When this option is selected, your IP address is hidden from senders and remote content is privately downloaded in the background when you receive a message (instead of when you view it)." In other words, on a device with MPP on, an email can look "opened" even if it was never read, because the content is pre-fetched in the background; the sender never sees the real open moment or IP.

That makes open rate unreliable for a mobile-heavy audience. This site's own Resend integration keeps click tracking on while treating open tracking as noisy because of MPP — trust click rate, not open rate, when making decisions.

Decision Framework: Volume, Region, Team, Pricing Model

Pricing tiers change over time, so check the current pricing page before deciding on an exact figure. That said, here are a few qualitative, documentation-verified axes to compare:

  • Infrastructure age: SendGrid's Node SDK requirement (Node ≥12) also works with older backends; Resend's Node SDK requires Node ≥20 — irrelevant if you're building a new backend, but SendGrid's broader compatibility can be an advantage if you're integrating into an older monolith.
  • API surface: Resend supports adding a domain through four paths — Dashboard/API/CLI/MCP server — a flexibility that stands out for teams that prefer an infrastructure-as-code approach (CLI/MCP) over SendGrid's more dashboard-heavy flow.
  • Trial length: SendGrid's official free trial quota is "100 emails/day for 60 days" — enough of a window to test a small mobile MVP's verification/password-reset traffic.
  • Team size: For small teams who want to keep domain authentication "in the background" with automatic SPF/DKIM/DMARC management, Twilio's "automated security" feature reduces operational overhead.

Laying the same axes side by side, the table looks like this:

Criterion
Resend
SendGrid
Node version requirement
≥20 (npm engines)
≥12 (npm engines)
Domain-add paths
Dashboard, API, CLI, MCP server
Dashboard-heavy, Domain Authentication flow
Free trial
Free tier available, includes 3 verified domains (Resend Changelog, Aug 25, 2026 — as of 2026-09-16)
100 emails/day for 60 days (official, as of 2026-09-16)
Automated security
Subdomain recommendation + manually added DNS records
SPF/DKIM/DMARC auto-managed via "Automated security"
API host/structure
Via SDK, resend.emails.send
Fixed host https://api.sendgrid.com/v3/, sgMail.send

This table doesn't declare a "winner" — both services do the same core job (domain verification, API-based sending, webhook-based monitoring) with different defaults. Choose based on your backend's age, your team's infrastructure preferences, and your trial timeline.

For a concrete number or a regional price comparison, check the current pricing page before deciding — this comparison covers only verified technical axes.

Testing and Monitoring

Monitoring is the step most often skipped after setup. A practical checklist:

  • Wire up the webhook endpoint early. You need to see bounce and complaint events before you go to production; otherwise every email you send to an invalid address disappears silently.
  • Keep a log on the backend side. Track which user got which template, and when, in your own database too — the service-side event can arrive late, or the webhook can temporarily go down.
  • Watch the rate limit headers. When SendGrid's X-RateLimit-Remaining header approaches zero, your queue-and-retry logic should kick in.
  • Keep PII out of the webhook payload. SendGrid's own docs warn about this explicitly; don't embed the user's email content or name into event metadata.

The two services' webhook payload shapes differ, so you need to write separate receiver code: Resend POSTs a single JSON event object per request ({ "type": ..., "data": { ... } }), while SendGrid sends an array of events, and the field name isn't type but event (e.g. "bounce", "spamreport"). Trying to loop over both with the same for...of block will throw a runtime error on the Resend request.

ts
1// Resend webhook receiver (Express) — a single event object is POSTed
2app.post("/webhooks/resend", (req, res) => {
3 const event = req.body;
4 if (event.type === "email.bounced" || event.type === "email.complained") {
5 // flag the user record, don't add to the resend list
6 }
7 res.status(200).send("ok");
8});
js
1// SendGrid Event Webhook receiver (Express) — an ARRAY of events is POSTed
2app.post("/webhooks/sendgrid", (req, res) => {
3 const events = req.body;
4 for (const e of events) {
5 if (e.event === "bounce" || e.event === "spamreport") {
6 // flag the user record, don't add to the resend list
7 }
8 }
9 res.status(200).send("ok");
10});

Before you go to production on the Resend side, you also need to set up webhook signature verification; otherwise you can't guarantee the payload really came from Resend. The rotateSigningSecret() method (Node SDK) and the rotate-webhook-signing-secret MCP server tool, both added September 9–11, 2026, let you rotate the signing secret without clicking through the dashboard whenever you suspect it leaked — especially useful for eliminating dashboard dependency in CI/CD or automation flows.

You need similar robustness on the rate-limit side: when SendGrid returns 429, you'll see the X-RateLimit-Remaining: 0 header. Instead of retrying immediately, apply exponential backoff and add your own idempotency key (e.g. user ID + operation type + timestamp) to every send request — otherwise a network timeout or retry can send the same verification code twice, since the email services won't deduplicate it for you.

GOLDEN TIP

The most valuable insight in this article

This tip holds the article's most important takeaway.

Easter Egg

You found a hidden gem!

There's a hidden detail in this section. Want to uncover it?

Reader Reward

I've pulled out the five checklist items you shouldn't forget after finishing setup — check each one off before you go live, especially the Cloudflare proxy and webhook steps, which get skipped most often.

FAQ

How do you send transactional email with Resend?

First you verify your domain with Resend (a subdomain is recommended, and the DNS record must be left without a proxy), then you create an API key, and then you call resend.emails.send({ from, to, subject, html }) in the Node SDK. Per the official docs, verification "usually" completes "within 15 minutes."

Why are SPF, DKIM, and DMARC records required?

Because without these records, the receiving server can't tell whether the mail sent on behalf of your domain is real or spoofed. Per SendGrid's official definition, these records prove domain ownership, sending permission, sender identity, and that the message wasn't tampered with in transit.

When should you choose Resend vs. SendGrid?

If you're building a new, modern Node backend (Node ≥20) and want an API/CLI/MCP-based infrastructure-as-code flow, Resend fits better. If you're integrating into an older backend (Node ≥12 is enough) or want enterprise-grade automated security (automatic SPF/DKIM/DMARC management), SendGrid's more established Twilio infrastructure can be an advantage.

What should I do if the verification-code email lands in spam?

First confirm domain authentication is complete (SPF/DKIM/DMARC all green), then make sure you're using a subdomain instead of the root domain, and finally check the bounce/complaint rate coming from your webhook — if it's high, review your send list and your template.

Why is open rate unreliable?

Because on devices with Apple Mail Privacy Protection turned on, content is pre-downloaded in the background before the user reads it, which makes even genuinely unread messages look "opened." That's why click rate is a more reliable signal.

Which events should I always listen for on the webhook?

At minimum, bounce and complaint events (email.bounced/email.complained on Resend, delivery/engagement events on SendGrid) — without these you can keep sending to invalid addresses and damage your reputation.

Update (September 2026)

This post is current as of 2026-09-16; in the three weeks before publishing, both services shipped changes worth tracking:

  • Resend — webhook signing secret rotation (Sept 9–11, 2026). The rotateSigningSecret() method was added to the Node SDK, and the rotate-webhook-signing-secret tool was added to the MCP server; the webhook creation flow was also redirected to the get-webhook endpoint. If you think your signing secret leaked in production, you can now rotate it from code or the MCP tool without clicking through the dashboard.
  • Resend — Broadcast/Template broken-link checking (Sept 3, 2026). Checking for broken, missing, or placeholder links before a bulk send was added — this affects the marketing Broadcast flow, not the transactional side, but it's worth knowing if you manage both from the same account.
  • Resend — team login SSO (Sept 1, 2026). SSO login via an IdP was enabled; this makes access control easier for teams where multiple people manage API keys/domains.
  • SendGrid — automatic SSL for Link Branding (Aug 27, 2026). Twilio announced the auto_ssl parameter and the auto_ssl_available field for branded links; the reason is that Gmail will start showing warnings on non-HTTPS links by the end of October 2026. If you use branded links in your emails, it's worth checking this setting.

None of these changes invalidate the core setup steps above (domain verification, API key, emails.send/sgMail.send calls); they add extra hardening on the webhook and link-security side.

Conclusion

Both Resend and SendGrid can reliably send your mobile backend's verification-code, password-reset, and order-notification emails; the real difference is setup philosophy and ecosystem. Getting domain verification (SPF/DKIM/DMARC) right, using a subdomain, and listening for bounce/complaint signals from the webhook matters more than which service you pick. To harden the network layer, see the network layer optimization guide (in Turkish); for synchronized data flows, see the CloudKit synchronization post; evaluating a serverless backend, check the Supabase Edge Functions guide or the Hono.js production guide. For general security hardening, see iOS Security Best Practices (in Turkish), and for offline-first backend patterns, see Firebase Advanced Patterns (in Turkish). To compare the two services side by side, check /comparisons/resend-vs-sendgrid/.

Sources

Tags

#resend#sendgrid#transactional email#spf dkim dmarc#node.js#webhook#email deliverability
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

Lead Mobile Engineer with 12+ years of experience. Expert in iOS, Android and cross-platform architectures with Swift, SwiftUI, Kotlin and Flutter. I build performant, user-friendly mobile apps.

Share