Overview

The Email API provides Resend-compatible email sending with organized namespaces for sent email management. Sent email operations are organized under email.sent.* for better structure, while top-level methods like email.send() and email.schedule() remain available for convenience.

Send Email

Send emails with full support for HTML, attachments, and headers:
import { Inbound } from '@inboundemail/sdk'

const inbound = new Inbound(process.env.INBOUND_API_KEY!)

// Send email using top-level convenience method
const { id } = await inbound.email.send({
  from: 'you@yourdomain.com', // we also support the "John Doe <john@doe.com>" format
  to: 'customer@example.com',
  subject: 'Welcome to our service',
  text: 'Thanks for signing up!',
  html: '<p>Thanks for signing up!</p>',
  tags: [{ name: 'campaign', value: 'welcome' }]
})

console.log('Email sent:', id)

Send Parameters

from
string
required
Sender email address. Can also include the sender display name in the format “John Doe <john@doe.com>”
to
string | string[]
required
Recipient email address(es)
subject
string
required
Email subject line
text
string
Plain text version of the email
html
string
HTML version of the email
cc
string | string[]
Carbon copy recipients
bcc
string | string[]
Blind carbon copy recipients
replyTo
string | string[]
Reply-to email address(es) - Changed from reply_to in v3.0.0
headers
object
Custom email headers
attachments
array
File attachments (see structure below)
tags
array
New in v3.0.0: Email tags for tracking and categorization

Attachment Structure

interface Attachment {
  content: string      // Base64 encoded content
  filename: string     // File name
  path?: string       // Alternative to content
  contentType?: string // MIME type
}

Tags Structure

interface Tag {
  name: string   // Tag name (e.g., "campaign", "priority")
  value: string  // Tag value (e.g., "welcome", "urgent")
}

Reply to Email

Reply to emails with context preservation:

Using the Simplified Reply Method

const inbound = new Inbound(process.env.INBOUND_API_KEY!)

// Reply with explicit from address
await inbound.reply(email, {
  from: 'support@yourdomain.com',
  text: 'Thanks for your message!'
})

// Reply with full options and tags
await inbound.reply(email, {
  from: 'support@yourdomain.com',
  text: 'Thanks for your message!',
  html: '<p>Thanks for your message!</p>',
  tags: [{ name: 'type', value: 'auto-reply' }],
  attachments: [{
    filename: 'invoice.pdf',
    content: base64Content,
    contentType: 'application/pdf'
  }]
})

Using the Full Reply API

// Reply via email ID with organized structure
await inbound.email.sent.reply('email_abc123', {
  from: 'support@yourdomain.com',
  text: 'Thank you for contacting us.',
  html: '<p>Thank you for contacting us.</p>',
  includeOriginal: true,
  tags: [{ name: 'department', value: 'support' }]
})

// Reply with all options
await inbound.email.sent.reply(emailId, {
  from: 'support@yourdomain.com',
  to: ['customer@example.com'],
  cc: ['manager@yourdomain.com'],
  subject: 'Re: Your inquiry',
  text: 'Here is our response...',
  html: '<p>Here is our response...</p>',
  replyTo: 'noreply@yourdomain.com',
  headers: {
    'X-Priority': '1'
  },
  attachments: [{
    filename: 'document.pdf',
    content: base64EncodedPDF,
    contentType: 'application/pdf'
  }],
  tags: [
    { name: 'priority', value: 'high' },
    { name: 'department', value: 'support' }
  ],
  includeOriginal: false
})

Reply Parameters

from
string
required
Sender email address
to
string | string[]
Override recipient addresses
cc
string | string[]
Carbon copy recipients
bcc
string | string[]
Blind carbon copy recipients
subject
string
Override subject line
text
string
Plain text reply content
html
string
HTML reply content
headers
object
Custom email headers
attachments
array
File attachments
includeOriginal
boolean
default:"false"
Include original email in reply
replyTo
string | string[]
Reply-to email address(es)
tags
array
Email tags for tracking and categorization

Practical Examples

Send Welcome Email

async function sendWelcomeEmail(userEmail: string, userName: string) {
  const { id } = await inbound.email.send({
    from: 'welcome@yourdomain.com',
    to: userEmail,
    subject: 'Welcome to Our Service!',
    html: `
      <h1>Welcome ${userName}!</h1>
      <p>We're excited to have you on board.</p>
      <a href="https://yourdomain.com/get-started">Get Started</a>
    `,
    text: `Welcome ${userName}! We're excited to have you on board.`,
    tags: [
      { name: 'campaign', value: 'welcome' },
      { name: 'user-type', value: 'new' }
    ]
  })
  
  return id
}

Send Email with Attachments

import fs from 'fs'

async function sendInvoice(customerEmail: string, invoicePath: string) {
  // Read and encode file
  const invoiceContent = fs.readFileSync(invoicePath)
  const base64Content = invoiceContent.toString('base64')
  
  const { id } = await inbound.email.send({
    from: 'billing@yourdomain.com',
    to: customerEmail,
    subject: 'Your Invoice',
    html: '<p>Please find your invoice attached.</p>',
    attachments: [{
      filename: 'invoice.pdf',
      content: base64Content,
      contentType: 'application/pdf'
    }],
    tags: [
      { name: 'type', value: 'invoice' },
      { name: 'department', value: 'billing' }
    ]
  })
  
  return id
}

Auto-Reply System

// In your webhook handler
export async function handleWebhook(payload: InboundWebhookPayload) {
  const { email } = payload
  
  // Check business hours
  const now = new Date()
  const hour = now.getHours()
  const isBusinessHours = hour >= 9 && hour < 17
  
  if (!isBusinessHours) {
    // Send auto-reply
    await inbound.reply(email, {
      from: 'support@yourdomain.com',
      html: `
        <p>Thank you for contacting us!</p>
        <p>We received your message outside of business hours. 
           Our team will respond within 24 hours.</p>
        <p>Business hours: Mon-Fri 9AM-5PM EST</p>
      `,
      text: 'Thank you for contacting us! We received your message...',
      tags: [
        { name: 'type', value: 'auto-reply' },
        { name: 'time', value: 'after-hours' }
      ]
    })
  }
}

Bulk Email Sending

async function sendNewsletter(subscribers: string[], content: string) {
  const results = []
  
  // Send in batches to respect rate limits
  const batchSize = 10
  for (let i = 0; i < subscribers.length; i += batchSize) {
    const batch = subscribers.slice(i, i + batchSize)
    
    const promises = batch.map(email => 
      inbound.email.send({
        from: 'newsletter@yourdomain.com',
        to: email,
        subject: 'Monthly Newsletter',
        html: content,
        headers: {
          'List-Unsubscribe': '<https://yourdomain.com/unsubscribe>'
        },
        tags: [
          { name: 'type', value: 'newsletter' },
          { name: 'campaign', value: 'monthly' }
        ]
      })
    )
    
    const batchResults = await Promise.allSettled(promises)
    results.push(...batchResults)
    
    // Rate limit delay
    await new Promise(resolve => setTimeout(resolve, 1000))
  }
  
  return results
}

Email Templates

interface EmailTemplate {
  subject: string
  html: string
  text: string
}

const templates: Record<string, EmailTemplate> = {
  orderConfirmation: {
    subject: 'Order Confirmation - #{orderNumber}',
    html: `
      <h1>Order Confirmed!</h1>
      <p>Hi {customerName},</p>
      <p>Your order #{orderNumber} has been confirmed.</p>
      <p>Total: ${amount}</p>
    `,
    text: 'Order Confirmed! Hi {customerName}...'
  }
}

async function sendTemplatedEmail(
  template: string,
  to: string,
  variables: Record<string, string>
) {
  const emailTemplate = templates[template]
  
  // Replace variables
  let html = emailTemplate.html
  let text = emailTemplate.text
  let subject = emailTemplate.subject
  
  Object.entries(variables).forEach(([key, value]) => {
    const regex = new RegExp(`{${key}}|#\{${key}\}`, 'g')
    html = html.replace(regex, value)
    text = text.replace(regex, value)
    subject = subject.replace(regex, value)
  })
  
  return inbound.email.send({
    from: 'noreply@yourdomain.com',
    to,
    subject,
    html,
    text
  })
}

Idempotency

Idempotency keys prevent duplicate email sends when requests are retried due to network issues, webhook failures, or application errors. When you include an idempotency key, the API will:
  1. First request: Process normally and store the key
  2. Duplicate requests: Return the original response without sending another email
Perfect for Webhooks: Webhook handlers often retry on failures. Idempotency keys ensure you only send one email per event, even if the webhook is called multiple times.

Using Idempotency with Email Sending

import { Inbound } from '@inboundemail/sdk'

const inbound = new Inbound(process.env.INBOUND_API_KEY!)

// Send email with idempotency key
const { data, error } = await inbound.email.send({
  from: 'support@yourdomain.com',
  to: 'customer@example.com',
  subject: 'Welcome to our service',
  text: 'Thanks for signing up!'
}, {
  idempotencyKey: 'welcome-user-123-v1'
})

if (error) {
  console.error('Send failed:', error)
} else {
  console.log('Email sent:', data.id)
}

Using Idempotency with Replies

// Reply with idempotency key using the organized API
const { data, error } = await inbound.email.sent.reply(
  'email_abc123',
  {
    from: 'support@yourdomain.com',
    text: 'Thanks for your message!'
  },
  {
    idempotencyKey: 'reply-abc123-user456-v1'
  }
)

// Using the simplified reply method
const { data, error } = await inbound.reply(
  'email_abc123',
  {
    from: 'support@yourdomain.com', 
    text: 'Thanks for your message!'
  },
  {
    idempotencyKey: 'quick-reply-' + Date.now()
  }
)

Idempotency Key Best Practices

1. Make Keys Unique and Meaningful

// ✅ GOOD: Include context and ensure uniqueness
const idempotencyKey = `reply-${emailId}-${userId}-${actionType}`

// ✅ BETTER: Include semantic information
const idempotencyKey = `support-reply-${emailId}-auto-v1`

// ✅ BEST: Use UUIDs for guaranteed uniqueness
import { v4 as uuid } from 'uuid'
const idempotencyKey = `reply-${emailId}-${uuid()}`

// ❌ BAD: Generic or non-unique
const idempotencyKey = 'email-123'

2. Webhook Handler with Idempotency

Perfect for preventing duplicate auto-replies:
import { NextRequest, NextResponse } from 'next/server'
import { Inbound } from '@inboundemail/sdk'
import type { InboundWebhookPayload } from '@inboundemail/sdk'

const inbound = new Inbound(process.env.INBOUND_API_KEY!)

export async function POST(request: NextRequest) {
  try {
    const payload: InboundWebhookPayload = await request.json()
    const { email } = payload
    
    // Create idempotency key from email ID and current timestamp
    // This ensures one auto-reply per email, even with webhook retries
    const idempotencyKey = `auto-reply-${email.id}-${Math.floor(Date.now() / 60000)}`
    
    const { data, error } = await inbound.reply(email.id, {
      from: 'support@yourdomain.com',
      text: 'Thank you for your email. We will respond within 24 hours.',
      includeOriginal: false
    }, {
      idempotencyKey
    })
    
    if (error) {
      console.error('Auto-reply failed:', error)
      return NextResponse.json({ error: 'Failed to send auto-reply' }, { status: 500 })
    }
    
    console.log('Auto-reply sent:', data.id)
    return NextResponse.json({ success: true, emailId: data.id })
    
  } catch (error) {
    console.error('Webhook processing failed:', error)
    return NextResponse.json({ error: 'Processing failed' }, { status: 500 })
  }
}

3. Retry Logic with Idempotency

Safely retry failed email sends:
async function sendEmailWithRetry(emailData: any, maxRetries = 3) {
  const idempotencyKey = `retry-send-${Date.now()}-${Math.random()}`
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const { data, error } = await inbound.email.send(emailData, {
        idempotencyKey // Same key for all retry attempts
      })
      
      if (!error) {
        console.log(`Email sent on attempt ${attempt}:`, data.id)
        return { success: true, data }
      }
      
      console.error(`Attempt ${attempt} failed:`, error)
      
    } catch (networkError) {
      console.error(`Network error on attempt ${attempt}:`, networkError)
    }
    
    // Don't wait after the last attempt
    if (attempt < maxRetries) {
      // Exponential backoff
      const delay = Math.pow(2, attempt) * 1000
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
  
  return { success: false, error: 'All retry attempts failed' }
}

// Usage
const result = await sendEmailWithRetry({
  from: 'orders@shop.com',
  to: 'customer@example.com',
  subject: 'Order Confirmation',
  text: 'Your order has been confirmed!'
})

4. Schedule Email with Idempotency

Prevent duplicate scheduled emails:
// Schedule reminder email with idempotency
async function scheduleOrderReminder(orderId: string, customerEmail: string) {
  const idempotencyKey = `order-reminder-${orderId}-v1`
  
  const { data, error } = await inbound.email.schedule({
    from: 'reminders@shop.com',
    to: customerEmail,
    subject: 'Don\'t forget your order!',
    html: '<p>You have items waiting in your cart.</p>',
    scheduled_at: '2024-12-25T10:00:00Z' // Christmas morning
  }, {
    idempotencyKey
  })
  
  if (error) {
    console.error('Failed to schedule reminder:', error)
    return null
  }
  
  console.log('Reminder scheduled:', data.id)
  return data.id
}

Idempotency Key Guidelines

1

Use descriptive prefixes

Include action type: reply-, welcome-, reminder-, notification-
2

Include relevant IDs

Add email ID, user ID, or order ID to make keys unique and traceable
3

Consider time windows

For time-sensitive operations, include timestamp or date to allow updates
4

Version your keys

Add version suffix (-v1, -v2) when you need to update email content
5

Keep keys under 256 characters

The API has a maximum length limit for idempotency keys

Idempotency Response Behavior

When using the same idempotency key:
// First request - email is sent
const result1 = await inbound.email.send({
  from: 'test@company.com',
  to: 'user@example.com',
  subject: 'Test',
  text: 'Hello!'
}, {
  idempotencyKey: 'test-key-123'
})
console.log('First request:', result1.data.id) // → "email_abc123"

// Second request with same key - returns original response, no new email sent
const result2 = await inbound.email.send({
  from: 'test@company.com',
  to: 'user@example.com', 
  subject: 'Test',
  text: 'Hello!'
}, {
  idempotencyKey: 'test-key-123' // Same key
})
console.log('Second request:', result2.data.id) // → "email_abc123" (same ID)
Idempotency keys expire after 24 hours. After expiration, the same key can be used for new operations.

Error Handling

try {
  await inbound.email.send({
    from: 'sender@yourdomain.com',
    to: 'recipient@example.com',
    subject: 'Test',
    text: 'Test email'
  })
} catch (error) {
  if (error.message.includes('401')) {
    console.error('Invalid API key')
  } else if (error.message.includes('422')) {
    console.error('Invalid email parameters')
  } else if (error.message.includes('429')) {
    console.error('Rate limit exceeded')
  } else {
    console.error('Failed to send email:', error.message)
  }
}

Resend Compatibility

The SDK is designed to be compatible with Resend’s API:
// Works like Resend
const { id } = await inbound.send({
  from: 'you@yourdomain.com',
  to: 'them@example.com',
  subject: 'Hello',
  html: '<p>Hello World</p>'
})

// Get sent email details
const email = await inbound.email.sent.get(id)
console.log('Email status:', email.last_event)

// OR use universal get (works for any email)
const email2 = await inbound.email.get(id)
console.log('Email status:', email2.last_event)

Next Steps