agentleFS
MoizIbnYousafMoizIbnYousaf/marketing-cli/skills/resend-inbound/SKILL.md

resend-inbound

Skill33 starsChanged 6 months agoMITView on GitHub

Use when receiving emails with Resend - setting up inbound domains, processing email.received webhooks, retrieving email content/attachments, or forwarding received emails. Triggers on "inbound email", "receive email", "email webhook", "Resend inbound", "process incoming email", "email forwarding", "email.received", "MX records for email".

  • Reads credentials

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 6 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈6

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Add content retrieval logic (body + attachments as needed).

    line 36
  2. Distinctive:

    Add routing logic if multiple recipients are expected.

    line 37
  3. Very specific:

    Use your auto-generated address: <anything>@<your-id>.resend.app

    line 51
  4. Distinctive:

    No DNS configuration needed.

    line 53
  5. Very specific:

    Add MX record to receive at <anything>@yourdomain.com.

    line 57
  6. Distinctive:

    If you already have MX records (e.g., Google Workspace, Microsoft 365):

    line 70
  7. Distinctive:

    If you set up Resend to receive email on a root domain, *all* traffic will be routed to Resend, not to any other mailbox.

    line 82
  8. Distinctive:

    Always verify signatures to prevent spoofed events:

    line 124
  9. Very specific:

    All emails to your domain arrive at the same webhook.

    line 251

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Receive Emails with Resend

Overview

Resend processes incoming emails for your domain and sends webhook events to your endpoint. Webhooks contain metadata only — you must call separate APIs to retrieve email body and attachments.

This skill is context-independent — it does not use brand/ files and works identically in any project.

On Activation

  1. Determine if the user needs a Resend-managed domain or custom domain.
  2. Walk through domain setup and webhook configuration.
  3. Implement webhook handler with signature verification.
  4. Add content retrieval logic (body + attachments as needed).
  5. Add routing logic if multiple recipients are expected.

Output: A webhook handler file with signature verification, content retrieval, and routing logic.

Quick Start

  1. Configure receiving domain — Use Resend's .resend.app domain or add MX record for custom domain
  2. Set up webhook — Subscribe to email.received event
  3. Retrieve content — Call Receiving API for body, Attachments API for files

Domain Setup

Option 1: Resend-Managed Domain (Fastest)

Use your auto-generated address: <anything>@<your-id>.resend.app

No DNS configuration needed. Find your address in Dashboard → Emails → Receiving → "Receiving address".

Option 2: Custom Domain

Add MX record to receive at <anything>@yourdomain.com.

SettingValue
TypeMX
HostYour domain or subdomain
ValueProvided in Resend dashboard
Priority10 (lowest number wins a conflict, but typically only multiples of 10 are used)

Critical: Your MX record must have the lowest priority value, or emails won't route to Resend.

Subdomain Recommendation

If you already have MX records (e.g., Google Workspace, Microsoft 365):

ApproachResult
Use subdomain (recommended)support.acme.com → Resend, acme.com → existing provider
Use root domainAll email routes to Resend (breaks existing email)
# Example: receive at support.acme.com without affecting acme.com
support.acme.com.  MX  10  <resend-mx-value>

If you set up Resend to receive email on a root domain, all traffic will be routed to Resend, not to any other mailbox. It's crucial, then, to use a subdomain with inbound emails.

Webhook Setup

Subscribe to email.received

Dashboard → Webhooks → Add Webhook → Select email.received

For local development, use tunneling (ngrok, VS Code Port Forwarding):

ngrok http 3000
# Use https://abc123.ngrok.io/api/webhook as endpoint

Webhook Payload Structure

Important: Payload contains metadata only, not email body or attachment content.

{
  "type": "email.received",
  "created_at": "2024-02-22T23:41:12.126Z",
  "data": {
    "email_id": "a1b2c3d4-...",
    "from": "sender@example.com",
    "to": ["support@acme.com"],
    "cc": [],
    "bcc": [],
    "subject": "Question about my order",
    "attachments": [
      {
        "id": "att_abc123",
        "filename": "receipt.pdf",
        "content_type": "application/pdf"
      }
    ]
  }
}

Verify Webhook Signatures

Always verify signatures to prevent spoofed events:

import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: Request) {
  const payload = await req.text();

  const event = resend.webhooks.verify({
    payload,
    headers: {
      'svix-id': req.headers.get('svix-id'),
      'svix-timestamp': req.headers.get('svix-timestamp'),
      'svix-signature': req.headers.get('svix-signature'),
    },
    secret: process.env.RESEND_WEBHOOK_SECRET,
  });

  if (event.type === 'email.received') {
    // Process the email
  }

  return new Response('OK', { status: 200 });
}

Retrieving Email Content

Webhooks exclude email body and headers. Call the Receiving API to get them:

if (event.type === 'email.received') {
  const { data: email } = await resend.emails.receiving.get(
    event.data.email_id
  );

  console.log(email.html);    // HTML body
  console.log(email.text);    // Plain text body
  console.log(email.headers); // Email headers
}

Why this design? Serverless environments have request body size limits. Separating content retrieval supports large emails and attachments.

Handling Attachments

Get Attachment Metadata and Download URLs

const { data: attachments } = await resend.emails.receiving.attachments.list({
  emailId: event.data.email_id,
});

for (const attachment of attachments) {
  console.log(attachment.filename);
  console.log(attachment.download_url);  // Valid for 1 hour
  console.log(attachment.expires_at);
}

Download Attachment Content

const response = await fetch(attachment.download_url);
const buffer = await response.arrayBuffer();

// Save to storage, process, etc.
await saveToStorage(attachment.filename, buffer);

Important: download_url expires after 1 hour. Call the API again for a fresh URL if needed.

Forwarding Emails

Complete workflow to receive and forward an email with attachments:

import { Resend } from 'resend';

const resend = new Resend(process.env.RESEND_API_KEY);

export async function POST(req: Request) {
  const payload = await req.text();
  const event = resend.webhooks.verify({ /* ... */ });

  if (event.type === 'email.received') {
    // 1. Get email content
    const { data: email } = await resend.emails.receiving.get(
      event.data.email_id
    );

    // 2. Get attachments (if any)
    const { data: attachmentList } = await resend.emails.receiving.attachments.list({
      emailId: event.data.email_id,
    });

    // 3. Download and encode attachments
    const attachments = await Promise.all(
      attachmentList.map(async (att) => {
        const res = await fetch(att.download_url);
        const buffer = Buffer.from(await res.arrayBuffer());
        return {
          filename: att.filename,
          content: buffer.toString('base64'),
        };
      })
    );

    // 4. Forward the email
    await resend.emails.send({
      from: 'Support System <system@acme.com>',
      to: ['team@acme.com'],
      subject: `Fwd: ${email.subject}`,
      html: email.html,
      text: email.text,
      attachments,
    });
  }

  return new Response('OK', { status: 200 });
}

Routing by Recipient

All emails to your domain arrive at the same webhook. Route based on the to field:

if (event.type === 'email.received') {
  const recipient = event.data.to[0];

  if (recipient.includes('support@')) {
    await handleSupportEmail(event.data);
  } else if (recipient.includes('billing@')) {
    await handleBillingEmail(event.data);
  } else {
    await handleUnknownEmail(event.data);
  }
}

Error Handling

FailureAction
Webhook signature verification failsReturn 400, log the attempt. Never process unverified webhooks.
resend.emails.receiving.get() returns errorLog the email_id, return 200 to acknowledge webhook, queue for retry via your own retry logic.
Attachment download_url expiredCall resend.emails.receiving.attachments.list() again for a fresh URL.
Attachment download times outRetry with exponential backoff (max 3 attempts). Log failure if all retries exhaust.
Malformed email (missing from/subject)Log and skip gracefully. Return 200 to prevent Resend retries on bad data.

Anti-Patterns

MistakeFix
Expecting body in webhook payloadWebhook has metadata only — call resend.emails.receiving.get() for body
MX record not lowest priorityEnsure Resend's MX has lowest number (highest priority)
Adding MX to root domain with existing emailUse subdomain to avoid breaking existing email service
Using expired download_urlURLs expire after 1 hour — call attachments API again for fresh URL
Not verifying webhook signaturesAlways verify — attackers can send fake events
Forgetting to return 200 OKResend retries on non-200 responses

Storage Note

Resend stores received emails even if:

  • Webhook isn't configured yet
  • Webhook endpoint is down

View all received emails in Dashboard → Emails → Receiving tab.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.