/email-with-resend
Consent-aware Resend email with queues, suppression, idempotency and signed webhooks.
Then type /email-with-resend in Claude Code, or describe the task and let it pick the skill.
What you get back contract tests · 10 Oct 2026
- A retry never sends twice
- User text escaped in HTML
- Bad address: no retry
- I've written the welcome and expense-split emails, but nothing sends them yet. Tallyroom has no signup flow, no database, and no "add expense" action; the dashboard shows hard-coded sample rows. So I built the email code as two server-side functions, and they need to be called from those handlers once they exist.
- Each email carries a fixed ID (one per user for welcome, one per expense and recipient for splits), so Resend won't send the same one twice if a call is retried.
- Bounces and spam complaints aren't handled.
RESEND_WEBHOOK_SECRETis already in.env.local, but without a database there's nowhere to record addresses to stop emailing.
Input → output from the run
Tallyroom needs email. When someone adds an expense, the people it's split with should get a short email, and new users should get a welcome email. Use Resend (key and sender are in .env.local).
/**
* Welcome email. Call once after the user row is created.
* user: { id, email, name? }
*/
export async function sendWelcomeEmail(user) {
const result = await sendEmail({
to: user.email,
...welcomeEmail({ name: user.name }),
idempotencyKey: `welcome:${user.id}`,
});
if (!result.ok) console.error("[email] welcome failed", { userId: user.id, ...result });
return result;
}When to use it
Use when an app needs transactional, lifecycle or campaign email through Resend.
Skip it for cold email to people who never signed up; that is cold-outreach.
The thinking behind itEmail is where trust is won or lost. Send with consent, suppression and retries handled, and the right message lands exactly once.
Output contract from the skill's Output section
Report message classes and triggers, files/templates/jobs changed, sender and environment-variable names, consent/preferences/suppression behavior, webhook and idempotency design, verification evidence/message IDs, and remaining DNS/dashboard/production checks.
How it checks its own work from the instructions
- Returned API errors and thrown network errors are both tested
- Webhooks tested with signed, invalid, and replayed payloads
- Bounce and complaint suppression is tested
- Sends go only to test recipients until production confirmed
Requirements what it needs
- Works in
- Codex, Claude Code, Cursor
- Needs
- project files, terminal commands, network access
The instructions SKILL.md · 30 lines
Instructions & references
Back to overview ↑On this page 4 sections
Email with Resend
Implement email as a reliable, permission-aware subsystem. This is not a cold-outreach workflow.
Workflow
- Inspect framework/runtime, installed Resend SDK version, queues/jobs, database and user fields, auth/admin roles, existing templates, email events, consent/preferences, suppression data, webhooks, and verified-domain configuration.
- Classify each requested message as transactional, security, lifecycle, or marketing. Do not scaffold re-engagement or campaigns by default. Ask about sender identity, jurisdiction/consent, and preference policy only when not established by the product.
- Read the Resend guide for current send/batch/webhook patterns, suppression handling, unsubscribe design, and copy guidance relevant to the selected class.
- Centralize provider calls. Detect the installed SDK API, handle both returned
errorvalues and thrown transport/runtime errors, use idempotency keys for event-triggered sends, and return a typed result to the caller. - Escape untrusted template values or use React/provider templates. Generate both HTML and useful text. Keep sender/reply-to configuration server-side and use a verified sending subdomain where appropriate.
- Do not block or silently abandon the primary transaction. Use a durable outbox/queue, or a runtime-supported post-response primitive with observability when loss is acceptable. Unawaited promises are not reliable in serverless runtimes.
- For campaigns, use contacts/audiences or a durable campaign job rather than a long request loop. Enforce consent, preferences, suppression, per-recipient idempotency, rate limits, cancellation, and an audit record. Require role-based admin authorization and confirmation of segment/count before send.
- Verify Resend webhook signatures over the raw body, deduplicate events, and process bounces/complaints/delivery failures into a suppression model. Never trust an unsigned event.
- Provide one-click unsubscribe and preference handling where required. Do not expose raw email addresses in URLs when an opaque signed identifier can be used. Keep transactional/security opt-outs separate from marketing preferences.
Verification
Test provider success plus returned API error, thrown network error, duplicate event, queue retry, invalid recipient, escaped user content, text rendering, verified sender/reply-to, signed/invalid/replayed webhook, bounce/complaint suppression, unsubscribe/preferences, unauthorized campaign access, segment preview, and rate-limit behavior. Run repository lint/type/test/build commands and send only to designated test recipients until production setup is confirmed.
Output
Report message classes and triggers, files/templates/jobs changed, sender and environment-variable names, consent/preferences/suppression behavior, webhook and idempotency design, verification evidence/message IDs, and remaining DNS/dashboard/production checks.
Resend implementation guide
Read only the sections needed for the selected transactional, lifecycle, or marketing email path. Check the installed Resend SDK and current primary documentation before using API field names.
Contents
- Send wrapper
- Templates and idempotency
- Durable delivery
- Campaigns
- Webhooks and suppression
- Preferences and unsubscribe
- Copy guidance
- Verification
Send wrapper
Current Resend Node SDK calls return { data, error } for API failures and may also throw for runtime/transport failures. The SDK uses replyTo in Node options.
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
type SendResult =
| { ok: true; id: string }
| { ok: false; retryable: boolean; message: string };
export async function sendEmail(input: {
to: string | string[];
subject: string;
html: string;
text: string;
idempotencyKey: string;
}): Promise<SendResult> {
try {
const { data, error } = await resend.emails.send(
{
from: process.env.EMAIL_FROM!,
replyTo: process.env.EMAIL_REPLY_TO,
to: input.to,
subject: input.subject,
html: input.html,
text: input.text,
},
{ idempotencyKey: input.idempotencyKey },
);
if (error || !data?.id) {
return classifyResendError(error);
}
return { ok: true, id: data.id };
} catch (error) {
return classifyTransportError(error);
}
}
Adapt the second-argument/idempotency signature to the installed SDK version. Do not return success merely because the promise resolved.
Templates and idempotency
Prefer React Email or provider templates for structured escaping. If generating HTML manually, escape every untrusted name, URL, and content value. Include a useful text alternative and one primary action.
Derive idempotency keys from the logical event, for example welcome:{userId}:{signupEventId}. Store send intent/status locally when delivery matters beyond Resend’s idempotency retention window.
Use a verified sending domain/subdomain and a monitored reply-to. SPF and DKIM are required for domain verification; add DMARC according to the domain’s delivery policy. Treat dashboard/DNS status as manual until observed.
Durable delivery
For user actions, write an outbox row in the same transaction as the triggering state change. A worker sends, records the Resend message ID, and retries classified transient failures with backoff. Use a dead-letter/alert path for permanent exhaustion.
Runtime-specific post-response primitives can be acceptable for low-value notifications, but an unawaited promise may be terminated in serverless environments. Do not hold a signup request open for network email delivery unless the email itself is the security transaction and the UX is designed for it.
Campaigns
Marketing/lifecycle email requires consent or another documented lawful basis and a product preference policy. Before send:
- materialize/preview the segment and recipient count;
- require role-based admin authorization and confirmation;
- create a campaign/run ID;
- enqueue one idempotent recipient job or use Resend Broadcasts/Contacts for marketing campaigns;
- filter current preferences and suppression at send time;
- record sent/skipped/failed counts without returning recipient PII broadly.
Resend’s Batch API can send up to the current documented limit per request and is suited to multiple transactional messages. Current Resend guidance recommends Broadcasts for marketing campaigns. Do not implement a long request loop with setTimeout as a campaign queue.
Webhooks and suppression
Verify Resend/Svix signatures using the raw request body and webhook secret before parsing/processing.
export async function POST(req: Request) {
const payload = await req.text();
let event;
try {
event = resend.webhooks.verify({
payload,
headers: {
id: req.headers.get('svix-id')!,
timestamp: req.headers.get('svix-timestamp')!,
signature: req.headers.get('svix-signature')!,
},
webhookSecret: process.env.RESEND_WEBHOOK_SECRET!,
});
} catch {
return new Response('Invalid signature', { status: 400 });
}
await processResendEventIdempotently(event);
return new Response('OK');
}
Use webhook/event ID for deduplication. data.to may contain multiple recipients; normalize addresses. Suppress hard bounces and complaints immediately, track transient delivery failures separately, and avoid equating a bounce with a user’s global marketing preference.
Preferences and unsubscribe
Model preferences by purpose/topic, with a global marketing opt-out and a separate suppression state. Security and essential transactional messages should not be disabled by a marketing opt-out.
Use an opaque, random or signed preference token that resolves server-side rather than placing raw email in the URL. Validate signatures in constant time, support key rotation/expiry policy, and provide a confirmation/preferences page. Add standards-compliant List-Unsubscribe and one-click behavior where applicable.
Copy guidance
- State why the recipient is receiving the message.
- Keep subject lines factual; avoid false urgency or fabricated personalization.
- One primary action is usually enough.
- Re-engagement should mention a real product change or user state, not placeholders.
- Do not infer “power”, “churned”, or “inactive” solely from simplistic percentiles; define segments from product behavior and validate queries.
Verification
- Test
{ data, error }and thrown-error paths. - Confirm idempotency prevents duplicate logical sends.
- Verify HTML escaping, text rendering, sender, reply-to, and test recipient delivery.
- Test valid/invalid/replayed webhooks and multi-recipient payloads.
- Test bounce/complaint suppression and preference/unsubscribe paths.
- Test unauthorized campaign access, segment preview, duplicate run, retry, cancellation, and rate limits.
Claude app, ChatGPT and ZIP download
local coding agent required
This skill requires project files, terminal commands, and network access. Uploading it to a chat app does not provide equivalent execution.
ChatGPT Skills
This workflow needs a local coding environment or capabilities that a chat-only Skills upload does not provide.
Package details
- Package
- email-with-resend/SKILL.md
- Author
- @tushaarmehtaa
- Added
- 7 Mar 2026
- Tags