Skip to main content

Send Transactional Email via Resend in a Cloud Function

In one sentence: Call resend.emails.send inside a CloudBase Web Cloud Function (HTTP trigger) to deliver transactional email from a verified sender domain; Resend handles SPF/DKIM/DMARC automatically, and the API key never reaches the frontend.

Estimated time: 30–60 minutes (DNS verification for your domain may take anywhere from 5 minutes to several hours depending on your DNS provider) | Difficulty: Advanced

Applicable Scenarios

  • One-to-one transactional emails: registration confirmation, password reset, order confirmation, billing alerts, or agent completion notifications
  • You want to avoid self-hosting SMTP (Postfix, SendGrid self-hosted) while maintaining deliverability and observability
  • Your users are primarily overseas, with minimal reliance on domestic Chinese mailboxes; or you are willing to validate deliverability to domestic mailboxes at low volume first

Not applicable:

  • Recipients are primarily on QQ Mail / 163 / 126 / Foxmail: overseas sending IPs are occasionally flagged as spam by these providers. Run a low-volume test first, and consider layering in a domestic email service (Tencent Cloud SES, Alibaba Cloud DirectMail) if needed.
  • You only need to send internal group notifications or alerts to IM users: see connect-wecom-webhook-cloud-function — email is overkill for that use case.
  • Marketing emails or bulk newsletters: Resend supports these, but this recipe covers transactional email only. Bulk sending involves unsubscribe flows and compliance requirements that are out of scope here.

Prerequisites

DependencyVersion / Requirement
Node.js (Cloud Function runtime)≥ 18
resend SDKlatest
@cloudbase/clilatest
Cloud Function typeWeb Cloud Function (HTTP trigger) — frontend / Mini Program calls it directly
Public network egressCloud Functions can access the public internet by default; confirm "Network → Public Access" is enabled in the Console
Resend accountThe free tier has a daily sending limit; check Resend pricing for current limits
Verified sender domainRequired (see Step 1 below); without verification, from can only be onboarding@resend.dev, which is for testing only

Step 1: Sign Up for Resend and Verify Your Sender Domain

Domain verification is the most time-consuming and failure-prone part of the entire flow — do this first.

  1. Log in to resend.comDomains in the left sidebar → Add Domain. Enter the domain you want to use as your sender (e.g. mail.yourdomain.com). Using a subdomain rather than your root domain is recommended to avoid polluting your main domain's email reputation.
  2. Resend will provide three DNS records. Use the exact values shown in the dashboard — never copy sample values from any documentation. The records typically include:
    • One MX record (points to Resend's MX server for bounce handling)
    • One TXT record (SPF: declares that Resend is authorized to send on behalf of your domain)
    • One CNAME or TXT record (DKIM: the public key for email signing)
  3. Add these three records to your domain's DNS provider (Cloudflare, Alibaba Cloud DNS, DNSPod, etc. all work). Pay attention to the host field format — Cloudflare expects just the subdomain prefix, while some providers expect the full hostname.
  4. Return to the Resend dashboard and click Verify. Status will progress from not_startedpendingverified. Propagation time ranges from a few minutes to a few hours depending on your DNS provider's TTL and caching.
  5. Once verified, DKIM and SPF are automatically signed by Resend — no action needed in the Cloud Function. If you want to add a DMARC policy as well, add a _dmarc TXT record, though this is optional.

To skip verification temporarily and just validate the flow: set from to onboarding@resend.dev. This only delivers to the email address bound to your Resend account and cannot be used for real users.

Step 2: Configure RESEND_API_KEY in Cloud Function Environment Variables

Go to the Resend dashboard → API KeysCreate API Key. Set the scope to Sending access and copy the resulting re_xxx... key. This key is only shown once — if you lose it, you must create a new one.

Then go to the CloudBase Console → "Cloud Functions → send-email → Environment Variables" and add:

  • RESEND_API_KEY: the re_xxx... key from above
  • RESEND_FROM: e.g. Acme <noreply@mail.yourdomain.com>. This must be an address under a verified domain and must use the Name <email> format — Resend rejects bare email addresses in the from field.

Never hardcode the key in your source code. Once committed to git, it is effectively exposed. Resend will auto-revoke it, but you will have already hit the problem. See secure-secrets-in-cloud-function for proper secrets management.

Step 3: Write the Cloud Function (Full resend.emails.send Call with Error Handling)

Initialize the project:

mkdir send-email && cd send-email
npm init -y
npm install resend express

Set "main": "index.js" in package.json and add "start": "node index.js" to scripts.

index.js:

const express = require('express');
const { Resend } = require('resend');

const app = express();
app.use(express.json({ limit: '10mb' }));

const RESEND_API_KEY = process.env.RESEND_API_KEY;
const RESEND_FROM = process.env.RESEND_FROM; // e.g. 'Acme <noreply@mail.yourdomain.com>'

if (!RESEND_API_KEY || !RESEND_FROM) {
console.error('Missing RESEND_API_KEY or RESEND_FROM env');
process.exit(1);
}

const resend = new Resend(RESEND_API_KEY);

app.post('/send', async (req, res) => {
const { to, subject, html, text, replyTo, cc, bcc, tags } = req.body || {};

// Validate required fields: to, subject, and at least one of html or text
if (!to || !subject || (!html && !text)) {
return res.status(400).json({ error: 'missing_fields', message: 'to, subject, and html or text are required' });
}

const { data, error } = await resend.emails.send({
from: RESEND_FROM,
to: Array.isArray(to) ? to : [to],
subject,
html,
text, // optional, plain-text fallback
replyTo, // optional, routes replies to a support address
cc, // optional, array
bcc, // optional, array
tags, // optional, [{ name: 'category', value: 'transactional' }] for dashboard filtering
headers: { 'X-Entity-Ref-ID': req.body?.refId || '' }, // optional, pass through your internal reference ID
});

if (error) {
// Resend error structure: { name: 'validation_error' | 'missing_required_field' | ..., message: '...' }
console.error('resend send failed', error);
return res.status(502).json({ error: error.name || 'resend_error', message: error.message });
}

// Success: data = { id: 'uuid' } — this ID is the Resend-side email ID, queryable in the dashboard
res.json({ ok: true, id: data?.id });
});

app.get('/health', (_req, res) => res.json({ ok: true }));

const PORT = process.env.PORT || 9000;
app.listen(PORT, () => {
console.log(`send-email listening on ${PORT}`);
});

A few common pitfalls:

  • The SDK expects to to be an array, but also accepts a single string — passing an array consistently avoids edge cases.
  • Without the Name <email> angle-bracket format in from, Resend returns validation_error immediately.
  • idempotencyKey is not shown in the example, but if your business layer can retry (e.g., failed queue consumers retrying delivery), add it: the same key within 24 hours sends the email only once, preventing users from receiving three identical messages.
  • scheduledAt accepts an ISO 8601 timestamp (UTC or with timezone offset) for deferred delivery — useful for "send a reminder at 9 AM tomorrow" scenarios.
  • Use if (error) for error handling rather than try/catch — the Resend SDK funnels both network failures and API errors into the error field. try/catch only catches synchronous SDK errors like missing parameters.

Step 4: Template Strategy — Raw HTML String vs. React Email Components

The simplest approach is to pass an HTML string directly in the html field:

html: `
<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 480px;">
<h2>Welcome to Acme</h2>
<p>Click the link below to verify your email address:</p>
<a href="${verifyUrl}" style="display: inline-block; padding: 10px 20px; background: #000; color: #fff; text-decoration: none; border-radius: 4px;">Verify Email</a>
</div>
`

This works, but email HTML compatibility is notoriously painful — Outlook ignores flex, Gmail strips half the <style> tags, and dark mode inverts colors unpredictably. For complex templates, React Email is the better option:

npm install @react-email/components @react-email/render
// templates/welcome.jsx
const { Html, Body, Container, Heading, Button } = require('@react-email/components');

function WelcomeEmail({ verifyUrl }) {
return (
<Html>
<Body style={{ fontFamily: 'sans-serif' }}>
<Container>
<Heading>Welcome to Acme</Heading>
<Button href={verifyUrl} style={{ background: '#000', color: '#fff', padding: '10px 20px' }}>
Verify Email
</Button>
</Container>
</Body>
</Html>
);
}

module.exports = WelcomeEmail;

Pass the component via the react field instead of html:

const WelcomeEmail = require('./templates/welcome');
const { data, error } = await resend.emails.send({
from: RESEND_FROM,
to: [userEmail],
subject: 'Verify your Acme account',
react: WelcomeEmail({ verifyUrl }),
});

The Resend SDK internally calls @react-email/render to convert the React component into compatibility-processed HTML — inlining styles, downgrading unsupported CSS, and generating a plain-text fallback. Make sure react and react-dom are listed as dependencies in package.json; React Email will not run without them.

Step 5: Deploy

tcb login
tcb fn deploy send-email --httpFn -e your-env-id

After deployment, go to the Console → "Cloud Functions → send-email" and do the following:

  1. Confirm Environment Variables RESEND_API_KEY and RESEND_FROM are set (skip if already done in Step 2).
  2. Confirm Network Configuration has public access enabled.
  3. Confirm Trigger Type has the HTTP access service enabled and note the URL, e.g. https://your-env.service.tcloudbase.com/send-email.

Verification

Send a test request from the command line to your own inbox:

curl -X POST 'https://your-env.service.tcloudbase.com/send-email/send' \
-H 'Content-Type: application/json' \
-d '{
"to": "yourself@gmail.com",
"subject": "CloudBase × Resend test",
"html": "<strong>It works!</strong>"
}'

Expected outcomes:

  • The function returns { "ok": true, "id": "xxxxxxx-uuid" }.
  • The email arrives in your inbox within 1–10 seconds, sent from the address configured in RESEND_FROM.
  • In the Resend dashboard → Logs, the email shows delivered status (sentdelivered). If it shows bounced or complained, the recipient's server rejected it.
  • In Gmail, open the email → three-dot menu → Show original. You should see dkim=pass and spf=pass. If either fails, revisit Step 1 and check the DNS records.

Common Errors

ErrorCauseFix
validation_error: The 'from' field must be a verified domainThe sender domain has not been verified in ResendGo back to Step 1, add the DNS records, and wait for the dashboard to show verified. For testing, temporarily use onboarding@resend.dev.
validation_error: from must be in format 'Name <email>'RESEND_FROM is set to a bare email like noreply@mail.yourdomain.comChange it to Acme <noreply@mail.yourdomain.com> — the display name and angle brackets are required.
restricted_api_key: This API key is restricted to only sending emailsWrong API key scope (e.g. Domains access selected instead of Sending access)Create a new key in the dashboard with Sending access scope.
Email delivered, but lands in spam on QQ Mail / 163 / 126Overseas sending IPs have lower reputation with domestic Chinese providers; newly created domains have no sending historyTighten DMARC policy (p=nonep=quarantine); warm up the domain with low-volume sends over a few days; guide users to whitelist the sender; for high-volume sends to domestic inboxes, consider switching to Tencent Cloud SES.
attachment_too_largeAttachment exceeds Resend's per-email size limit (40 MB total)Use object storage and send a download link instead, or split into multiple emails. Check the Resend attachments docs for the current limit.
rate_limit_exceededToo many sends in a short period; default limit is 2 emails per second (can be increased via the dashboard)Add a queue on the business side to smooth out spikes; use resend.batch.send for bulk sends (up to 100 per call); contact Resend support to raise your limit.
invalid_idempotency_keyA retry used the same idempotencyKey as a previous successful send but with different parametersThe same key must always map to the same parameters; use a new key for different emails.
getaddrinfo EAI_AGAIN api.resend.comCloud Function has no public network egressEnable public access in the Console; if the region restricts outbound traffic, attach an egress NAT.
Function times out after 30 secondsDefault function execution timeout is 30 seconds; slow Resend responses or retries can hit thisConsole → Function Config → Timeout, increase to 60 seconds; or offload sending to a queue for async processing.

For the full error code reference, see docs.cloudbase.net error codes and the Resend errors reference.