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
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)
Plain text version of the email
HTML version of the email
Blind carbon copy recipients
Reply-to email address(es) - Changed from reply_to
in v3.0.0
File attachments (see structure below)
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
}
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
Override recipient addresses
Blind carbon copy recipients
Include original email in reply
Reply-to email address(es)
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:
- First request: Process normally and store the key
- 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
Use descriptive prefixes
Include action type: reply-
, welcome-
, reminder-
, notification-
Include relevant IDs
Add email ID, user ID, or order ID to make keys unique and traceable
Consider time windows
For time-sensitive operations, include timestamp or date to allow updates
Version your keys
Add version suffix (-v1
, -v2
) when you need to update email content
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