skills$openclaw/resend-inbound
christina-de-martinez4.4k

by christina-de-martinez

resend-inbound – OpenClaw Skill

resend-inbound is an OpenClaw Skills integration for coding workflows. Use when receiving emails with Resend - setting up inbound domains, processing email.received webhooks, retrieving email content/attachments, or forwarding received emails.

4.4k stars7.2k forksSecurity L1
Updated Feb 7, 2026Created Feb 7, 2026coding

Skill Snapshot

nameresend-inbound
descriptionUse when receiving emails with Resend - setting up inbound domains, processing email.received webhooks, retrieving email content/attachments, or forwarding received emails. OpenClaw Skills integration.
ownerchristina-de-martinez
repositorychristina-de-martinez/resend-skillspath: resend-inbound
languageMarkdown
licenseMIT
topics
securityL1
installopenclaw add @christina-de-martinez/resend-skills:resend-inbound
last updatedFeb 7, 2026

Maintainer

christina-de-martinez

christina-de-martinez

Maintains resend-inbound in the OpenClaw Skills directory.

View GitHub profile
File Explorer
1 files
resend-inbound
SKILL.md
7.2 KB
SKILL.md

name: resend-inbound description: Use when receiving emails with Resend - setting up inbound domains, processing email.received webhooks, retrieving email content/attachments, or forwarding received emails.

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.

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);
  }
}

Common Mistakes

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.

README.md

No README available.

Permissions & Security

Security level L1: Low-risk skills with minimal permissions. Review inputs and outputs before running in production.

Requirements

  • OpenClaw CLI installed and configured.
  • Language: Markdown
  • License: MIT
  • Topics:

FAQ

How do I install resend-inbound?

Run openclaw add @christina-de-martinez/resend-skills:resend-inbound in your terminal. This installs resend-inbound into your OpenClaw Skills catalog.

Does this skill run locally or in the cloud?

OpenClaw Skills execute locally by default. Review the SKILL.md and permissions before running any skill.

Where can I verify the source code?

The source repository is available at https://github.com/openclaw/skills/tree/main/skills/christina-de-martinez/resend-skills. Review commits and README documentation before installing.