Skip to main content

Overview

Inbound API returns structured error responses with appropriate HTTP status codes. Understanding these errors and implementing proper error handling is crucial for building reliable integrations.

Error Response Structure

All error responses follow a consistent structure:
interface ErrorResponse {
  error: string          // Human-readable error message
  details?: string       // Additional error details (optional)
  code?: string         // Machine-readable error code (optional)
}

Example Error Response

{
  "error": "Invalid email address format",
  "details": "The email address 'invalid-email' does not match the required format"
}

HTTP Status Codes

Inbound uses standard HTTP status codes to indicate the success or failure of requests:

2xx Success

CodeDescriptionUsage
200OKSuccessful GET, PATCH, DELETE requests
201CreatedSuccessful POST request (resource created)

4xx Client Errors

CodeDescriptionUsage
400Bad RequestInvalid request parameters or validation errors
401UnauthorizedMissing or invalid API key
403ForbiddenValid API key but insufficient permissions
404Not FoundResource doesn’t exist or not accessible
409ConflictResource already exists or conflict detected
429Too Many RequestsRate limit exceeded

5xx Server Errors

CodeDescriptionUsage
500Internal Server ErrorUnexpected server error
503Service UnavailableService temporarily unavailable

Common Errors

400 Bad Request

Returned when request parameters are invalid or missing required fields.
{
  "error": "Missing required fields: from, to, and subject are required"
}
Common causes:
  • Missing required fields (from, to, subject)
  • Invalid email address format
  • Invalid domain format
  • Email content too large
  • Invalid scheduled date format

401 Unauthorized

Returned when authentication fails.
{
  "error": "Unauthorized"
}
Common causes:
  • API key missing from Authorization header
  • Invalid API key format
  • API key deleted or revoked
  • Missing “Bearer” prefix
See the Authentication guide for detailed troubleshooting.

403 Forbidden

Returned when you don’t have permission to perform the action.
{
  "error": "You don't have permission to send from domain: example.com"
}
Common causes:
  • Domain not verified
  • Domain belongs to another user
  • API key lacks required permissions
  • Domain limit reached

404 Not Found

Returned when the requested resource doesn’t exist.
{
  "error": "Not found"
}
Common causes:
  • Resource ID doesn’t exist
  • Resource was deleted
  • Resource belongs to another user
  • Typo in resource ID

409 Conflict

Returned when a resource already exists or there’s a conflict.
{
  "error": "You have already added this domain to your account"
}
Or:
{
  "error": "This domain is already registered on our platform. If you believe this is an error or you need to transfer ownership, please contact our support team.",
  "code": "DOMAIN_ALREADY_REGISTERED"
}
Common causes:
  • Domain already added to your account
  • Domain registered by another user
  • Email address already exists
  • Duplicate resource creation

429 Too Many Requests

Returned when rate limits are exceeded.
{
  "error": "Rate limit exceeded"
}
Or:
{
  "error": "Email sending limit reached. Please upgrade your plan to send more emails."
}
Common causes:
  • Too many requests per second
  • Monthly email limit reached
  • Plan limits exceeded
Check the Retry-After header to know when to retry.

500 Internal Server Error

Returned when an unexpected server error occurs.
{
  "error": "Failed to send email. Please try again later."
}
Common causes:
  • Temporary service disruption
  • Database connection issues
  • External service failure (AWS SES, etc.)

Try/Catch Patterns

Basic Error Handling

import { Inbound } from 'inboundemail'

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

try {
  const email = await inbound.emails.send({
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Welcome',
    html: '<p>Welcome to our service!</p>'
  })
  
  console.log(`✅ Email sent: ${email.id}`)
} catch (error) {
  console.error('❌ Failed to send email:', error.message)
  
  // Log full error for debugging
  console.error('Status:', error.status)
  console.error('Body:', error.body)
}

Comprehensive Error Handling

async function sendEmail(emailData: any) {
  try {
    const email = await inbound.emails.send(emailData)
    return { success: true, data: email }
  } catch (error) {
    // Handle specific error types
    switch (error.status) {
      case 400:
        return { 
          success: false, 
          error: 'VALIDATION_ERROR',
          message: 'Invalid email data: ' + error.body.error 
        }
      
      case 401:
        return { 
          success: false, 
          error: 'AUTH_ERROR',
          message: 'Authentication failed. Check your API key.' 
        }
      
      case 403:
        return { 
          success: false, 
          error: 'PERMISSION_ERROR',
          message: 'Permission denied: ' + error.body.error 
        }
      
      case 429:
        return { 
          success: false, 
          error: 'RATE_LIMIT',
          message: 'Rate limit exceeded. Please try again later.',
          retryAfter: error.headers?.get('Retry-After')
        }
      
      case 500:
        return { 
          success: false, 
          error: 'SERVER_ERROR',
          message: 'Server error. Please try again later.' 
        }
      
      default:
        return { 
          success: false, 
          error: 'UNKNOWN_ERROR',
          message: error.message 
        }
    }
  }
}

// Usage
const result = await sendEmail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Welcome',
  html: '<p>Welcome!</p>'
})

if (result.success) {
  console.log('Email sent:', result.data.id)
} else {
  console.error('Failed:', result.error, result.message)
}

Typed Error Handling

interface InboundError extends Error {
  status: number
  body: {
    error: string
    details?: string
    code?: string
  }
  headers?: Headers
}

function isInboundError(error: unknown): error is InboundError {
  return (
    error instanceof Error &&
    'status' in error &&
    'body' in error
  )
}

try {
  const email = await inbound.emails.send({ /* ... */ })
} catch (error) {
  if (isInboundError(error)) {
    // TypeScript knows about error.status, error.body, etc.
    console.error(`Error ${error.status}: ${error.body.error}`)
  } else {
    // Unknown error type
    console.error('Unexpected error:', error)
  }
}

Retry Strategies

Exponential Backoff

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error) {
      const isLastAttempt = attempt === maxRetries - 1
      const isRetryable = error.status >= 500 || error.status === 429
      
      if (!isRetryable || isLastAttempt) {
        throw error
      }
      
      // Exponential backoff: 1s, 2s, 4s, 8s, ...
      const delay = baseDelay * Math.pow(2, attempt)
      const jitter = Math.random() * 1000 // Add jitter to prevent thundering herd
      
      console.log(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`)
      await new Promise(resolve => setTimeout(resolve, delay + jitter))
    }
  }
  
  throw new Error('Max retries exceeded')
}

// Usage
const email = await retryWithBackoff(() =>
  inbound.emails.send({
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Welcome',
    html: '<p>Welcome!</p>'
  })
)

Retry Specific Errors

const RETRYABLE_ERRORS = [429, 500, 503]

async function retryRequest<T>(
  fn: () => Promise<T>,
  options = { maxRetries: 3, delay: 1000 }
): Promise<T> {
  for (let attempt = 0; attempt < options.maxRetries; attempt++) {
    try {
      return await fn()
    } catch (error) {
      if (isInboundError(error)) {
        const shouldRetry = RETRYABLE_ERRORS.includes(error.status)
        const isLastAttempt = attempt === options.maxRetries - 1
        
        if (!shouldRetry || isLastAttempt) {
          throw error
        }
        
        // Use Retry-After header if available
        const retryAfter = error.headers?.get('Retry-After')
        const delay = retryAfter 
          ? parseInt(retryAfter) * 1000 
          : options.delay * Math.pow(2, attempt)
        
        console.log(`Retrying in ${delay}ms (attempt ${attempt + 1})...`)
        await new Promise(resolve => setTimeout(resolve, delay))
      } else {
        throw error
      }
    }
  }
  
  throw new Error('Max retries exceeded')
}

Circuit Breaker Pattern

class CircuitBreaker {
  private failures = 0
  private lastFailureTime = 0
  private state: 'closed' | 'open' | 'half-open' = 'closed'
  
  constructor(
    private threshold = 5,
    private timeout = 60000
  ) {}
  
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      const now = Date.now()
      if (now - this.lastFailureTime > this.timeout) {
        this.state = 'half-open'
      } else {
        throw new Error('Circuit breaker is open')
      }
    }
    
    try {
      const result = await fn()
      this.onSuccess()
      return result
    } catch (error) {
      this.onFailure()
      throw error
    }
  }
  
  private onSuccess() {
    this.failures = 0
    this.state = 'closed'
  }
  
  private onFailure() {
    this.failures++
    this.lastFailureTime = Date.now()
    
    if (this.failures >= this.threshold) {
      this.state = 'open'
      console.log('Circuit breaker opened')
    }
  }
}

// Usage
const breaker = new CircuitBreaker(5, 60000)

try {
  const email = await breaker.execute(() =>
    inbound.emails.send({ /* ... */ })
  )
} catch (error) {
  console.error('Request failed:', error.message)
}

Error Monitoring

Logging Errors

import * as Sentry from '@sentry/node'

try {
  const email = await inbound.emails.send({ /* ... */ })
} catch (error) {
  if (isInboundError(error)) {
    // Log to monitoring service
    Sentry.captureException(error, {
      tags: {
        service: 'inbound',
        status: error.status,
        endpoint: 'emails.send'
      },
      extra: {
        errorBody: error.body,
        requestData: { /* sanitized request data */ }
      }
    })
  }
  
  throw error
}

Structured Logging

import { logger } from './logger'

try {
  const email = await inbound.emails.send({ /* ... */ })
  
  logger.info('Email sent successfully', {
    emailId: email.id,
    messageId: email.message_id,
    from: emailData.from,
    to: emailData.to
  })
} catch (error) {
  if (isInboundError(error)) {
    logger.error('Failed to send email', {
      status: error.status,
      error: error.body.error,
      details: error.body.details,
      code: error.body.code,
      requestData: emailData
    })
  }
}

Real Error Examples

From the source code, here are actual error scenarios:

Invalid Email Format

// Source: app/api/e2/emails/send.ts:320-330
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
for (const email of allRecipients) {
  const address = extractEmailAddress(email)
  if (!emailRegex.test(address)) {
    // Returns 400
    throw new Error(`Invalid email format: ${email}`)
  }
}

Domain Ownership

// Source: app/api/e2/emails/send.ts:301-308
if (userDomain.length === 0) {
  // Returns 403
  throw new Error(`You don't have permission to send from domain: ${fromDomain}`)
}

Rate Limit Exceeded

// Source: app/api/e2/emails/send.ts:381-388
if (!emailCheck.allowed) {
  // Returns 429
  throw new Error('Email sending limit reached. Please upgrade your plan to send more emails.')
}

Resource Conflict

// Source: app/api/e2/domains/create.ts:128-142
if (existingDomainAnyUser[0]) {
  if (isOwnDomain) {
    // Returns 409
    throw new Error('You have already added this domain to your account')
  } else {
    // Returns 409 with code
    throw new Error(
      'This domain is already registered on our platform. If you believe this is an error or you need to transfer ownership, please contact our support team.',
      { code: 'DOMAIN_ALREADY_REGISTERED' }
    )
  }
}

Next Steps

TypeScript SDK

Explore the full SDK reference

Authentication

Secure your API requests

Webhooks

Configure webhooks for receiving emails

Rate Limits

Understand rate limits and quotas

Build docs developers (and LLMs) love