Skip to main content

Overview

Inbound processes incoming emails and sends them to your webhook endpoint as structured JSON payloads. This guide shows you how to set up and handle incoming emails.

How It Works

1

Configure Email Address

Create an email address and specify your webhook URL:
const emailAddress = await inbound.emailAddresses.create({
  address: 'support@yourdomain.com',
  webhookUrl: 'https://yourapp.com/api/webhooks/inbound'
})
2

Receive Email

When someone sends an email to support@yourdomain.com, Inbound:
  1. Receives and parses the email
  2. Extracts headers, body, and attachments
  3. Sends a POST request to your webhook URL
3

Process in Your App

Your webhook handler receives the structured email data and processes it.

Webhook Payload

Every incoming email triggers a POST request with this structure:
{
  "type": "email.received",
  "email": {
    "id": "email_abc123",
    "message_id": "<CABcd123@mail.gmail.com>",
    "subject": "Question about pricing",
    "from": {
      "text": "John Doe <john@example.com>",
      "addresses": [
        {
          "name": "John Doe",
          "address": "john@example.com"
        }
      ]
    },
    "to": {
      "text": "support@yourdomain.com",
      "addresses": [
        {
          "address": "support@yourdomain.com"
        }
      ]
    },
    "date": "2026-02-19T10:30:00.000Z",
    "text_body": "Hi, I have a question about your pricing plans...",
    "html_body": "<p>Hi, I have a question about your pricing plans...</p>",
    "attachments": [],
    "headers": {
      "received": ["from mail.example.com..."],
      "content-type": "text/plain; charset=UTF-8"
    }
  }
}

Next.js Webhook Handler

App Router (Next.js 13+)

app/api/webhooks/inbound/route.ts
import { Inbound, type InboundWebhookPayload, isInboundWebhookPayload } from 'inboundemail'
import { NextRequest, NextResponse } from 'next/server'

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

export async function POST(request: NextRequest) {
  try {
    const payload: InboundWebhookPayload = await request.json()
    
    // Verify this is a valid Inbound webhook
    if (!isInboundWebhookPayload(payload)) {
      console.error('Invalid webhook payload')
      return NextResponse.json(
        { error: 'Invalid webhook' },
        { status: 400 }
      )
    }
    
    const { email } = payload
    
    console.log('Received email:', {
      id: email.id,
      from: email.from?.addresses?.[0]?.address,
      subject: email.subject
    })
    
    // Extract sender info
    const senderEmail = email.from?.addresses?.[0]?.address || 'unknown'
    const senderName = email.from?.addresses?.[0]?.name || senderEmail
    
    // Process based on subject or content
    if (email.subject?.toLowerCase().includes('support')) {
      await handleSupportEmail(email)
    } else if (email.subject?.toLowerCase().includes('billing')) {
      await handleBillingEmail(email)
    } else {
      await handleGeneralEmail(email)
    }
    
    return NextResponse.json({ success: true })
  } catch (error) {
    console.error('Webhook error:', error)
    return NextResponse.json(
      { error: 'Webhook processing failed' },
      { status: 500 }
    )
  }
}

async function handleSupportEmail(email: any) {
  // Create support ticket
  console.log('Creating support ticket for:', email.subject)
  
  // Auto-reply
  await inbound.reply(email, {
    from: 'support@yourdomain.com',
    text: 'Thanks for contacting support! We\'ll get back to you within 24 hours.',
    tags: [{ name: 'type', value: 'auto-reply' }]
  })
}

async function handleBillingEmail(email: any) {
  // Forward to billing team
  console.log('Forwarding billing inquiry')
}

async function handleGeneralEmail(email: any) {
  // Log for review
  console.log('General inquiry received')
}

Pages Router (Next.js 12)

pages/api/webhooks/inbound.ts
import type { NextApiRequest, NextApiResponse } from 'next'
import { Inbound, type InboundWebhookPayload, isInboundWebhookPayload } from 'inboundemail'

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

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    const payload: InboundWebhookPayload = req.body
    
    if (!isInboundWebhookPayload(payload)) {
      return res.status(400).json({ error: 'Invalid webhook' })
    }
    
    const { email } = payload
    console.log('Received email from:', email.from?.addresses?.[0]?.address)
    
    // Process email
    await processIncomingEmail(email)
    
    return res.status(200).json({ success: true })
  } catch (error) {
    console.error('Webhook error:', error)
    return res.status(500).json({ error: 'Processing failed' })
  }
}

async function processIncomingEmail(email: any) {
  // Your email processing logic
}

Express.js Handler

const express = require('express')
const { Inbound, isInboundWebhookPayload } = require('inboundemail')

const app = express()
const inbound = new Inbound(process.env.INBOUND_API_KEY)

app.use(express.json())

app.post('/webhooks/inbound', async (req, res) => {
  try {
    const payload = req.body
    
    if (!isInboundWebhookPayload(payload)) {
      return res.status(400).json({ error: 'Invalid webhook' })
    }
    
    const { email } = payload
    
    console.log('📧 New email received:', {
      id: email.id,
      from: email.from?.addresses?.[0]?.address,
      subject: email.subject,
      hasAttachments: email.attachments?.length > 0
    })
    
    // Extract text body
    const textBody = email.text_body || ''
    
    // Check for keywords
    if (textBody.toLowerCase().includes('urgent')) {
      await handleUrgentEmail(email)
    }
    
    // Auto-reply to new contacts
    const isNewContact = await checkIfNewContact(email.from?.addresses?.[0]?.address)
    if (isNewContact) {
      await inbound.reply(email, {
        from: 'hello@yourdomain.com',
        html: '<p>Thanks for reaching out! We\'ll respond soon.</p>'
      })
    }
    
    res.json({ success: true })
  } catch (error) {
    console.error('Webhook processing error:', error)
    res.status(500).json({ error: 'Processing failed' })
  }
})

app.listen(3000, () => {
  console.log('Webhook server running on port 3000')
})

Extracting Data

Sender Information

const { email } = payload

// Get sender email
const senderEmail = email.from?.addresses?.[0]?.address || 'unknown'

// Get sender name (if provided)
const senderName = email.from?.addresses?.[0]?.name || null

// Full formatted sender
const senderFull = email.from?.text || senderEmail

console.log('From:', senderName ? `${senderName} <${senderEmail}>` : senderEmail)

Recipients

// Primary recipients
const toAddresses = email.to?.addresses?.map(addr => addr.address) || []

// CC recipients
const ccAddresses = email.cc?.addresses?.map(addr => addr.address) || []

// BCC recipients (usually not visible in received emails)
const bccAddresses = email.bcc?.addresses?.map(addr => addr.address) || []

console.log('Recipients:', { to: toAddresses, cc: ccAddresses })

Subject and Body

const subject = email.subject || 'No Subject'
const textBody = email.text_body || ''
const htmlBody = email.html_body || null

// Text preview (first 100 characters)
const preview = textBody.substring(0, 100)

console.log('Subject:', subject)
console.log('Preview:', preview)

Attachments

const hasAttachments = email.attachments && email.attachments.length > 0

if (hasAttachments) {
  email.attachments.forEach(attachment => {
    console.log('Attachment:', {
      filename: attachment.filename,
      contentType: attachment.content_type,
      size: attachment.size
    })
  })
}
Learn more about downloading attachments in the Attachments Guide.

Headers

// Access specific headers
const messageId = email.message_id
const date = email.date
const inReplyTo = email.headers?.['in-reply-to'] || null
const references = email.headers?.references || []

console.log('Message-ID:', messageId)
console.log('In-Reply-To:', inReplyTo)

Common Use Cases

Auto-Reply System

export async function POST(request: NextRequest) {
  const payload = await request.json()
  
  if (!isInboundWebhookPayload(payload)) {
    return NextResponse.json({ error: 'Invalid webhook' }, { status: 400 })
  }
  
  const { email } = payload
  
  // Auto-reply to all emails
  await inbound.reply(email, {
    from: 'support@yourdomain.com',
    text: `
Hi ${email.from?.addresses?.[0]?.name || 'there'},

Thanks for your email! We've received your message and will respond within 24 hours.

Best regards,
Support Team
    `.trim()
  })
  
  return NextResponse.json({ success: true })
}

Support Ticket Creation

async function createSupportTicket(email: any) {
  const ticket = {
    title: email.subject,
    description: email.text_body,
    customer_email: email.from?.addresses?.[0]?.address,
    customer_name: email.from?.addresses?.[0]?.name,
    attachments: email.attachments?.map(a => a.filename),
    received_at: email.date,
    email_id: email.id
  }
  
  // Save to your database
  const savedTicket = await db.tickets.create(ticket)
  
  // Send confirmation
  await inbound.reply(email, {
    from: 'support@yourdomain.com',
    html: `
      <p>Hi ${ticket.customer_name || 'there'},</p>
      <p>We've created ticket <strong>#${savedTicket.id}</strong> for your inquiry.</p>
      <p>You can track the status at: <a href="https://yourapp.com/tickets/${savedTicket.id}">View Ticket</a></p>
    `
  })
  
  return savedTicket
}

Email Forwarding

export async function POST(request: NextRequest) {
  const payload = await request.json()
  const { email } = payload
  
  // Forward to team
  await inbound.emails.send({
    from: 'forwarding@yourdomain.com',
    to: 'team@yourdomain.com',
    subject: `FWD: ${email.subject}`,
    text: `
Forwarded message from ${email.from?.text}:

${email.text_body}
    `.trim()
  })
  
  return NextResponse.json({ success: true })
}

Error Handling

export async function POST(request: NextRequest) {
  try {
    const payload = await request.json()
    
    if (!isInboundWebhookPayload(payload)) {
      console.error('Invalid webhook payload structure')
      return NextResponse.json(
        { error: 'Invalid webhook' },
        { status: 400 }
      )
    }
    
    const { email } = payload
    
    // Validate required fields
    if (!email.id || !email.from?.addresses?.[0]?.address) {
      console.error('Missing required email fields')
      return NextResponse.json(
        { error: 'Incomplete email data' },
        { status: 400 }
      )
    }
    
    await processEmail(email)
    
    return NextResponse.json({ success: true })
  } catch (error) {
    console.error('Webhook processing error:', error)
    
    // Return 200 to prevent retries for permanent errors
    return NextResponse.json(
      { success: false, error: 'Processing failed' },
      { status: 200 }
    )
  }
}
Return 200 for processed webhooks - Even if your processing fails, return a 200 status to prevent Inbound from retrying. Log errors to your monitoring system instead.

Testing Webhooks Locally

Using ngrok

1

Install ngrok

brew install ngrok
# or download from ngrok.com
2

Start your local server

npm run dev
# Server running on http://localhost:3000
3

Create tunnel

ngrok http 3000
Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
4

Update webhook URL

await inbound.emailAddresses.update('addr_123', {
  webhookUrl: 'https://abc123.ngrok.io/api/webhooks/inbound'
})
5

Send test email

Send an email to your email address and watch the webhook arrive!

Next Steps

Replying to Emails

Reply to incoming emails with threading

Attachments

Download and process email attachments

Webhook Security

Verify webhook signatures

Email Threads

View conversations and threads

Build docs developers (and LLMs) love