Inbound Processing

Learn how to receive and process inbound emails with webhooks.

Overview

Inbound email processing allows your application to receive emails programmatically. When someone sends an email to your inbound address, Transactional processes it and delivers the parsed data to your webhook endpoint.

How It Works

  1. Email Arrives — An email is sent to your inbound address
  2. Processing — Transactional processes the email:
    • Parses headers, body, and attachments
    • Runs spam and malware checks
    • Validates authentication (SPF, DKIM, DMARC)
  3. Webhook Delivery — Parsed data is sent to your endpoint:
    • JSON payload with all email data
    • HMAC signature for verification
    • Automatic retries on failure

Setting Up Inbound Processing

Step 1: Create an Inbound Stream

  1. Navigate to your email server in the dashboard
  2. Click Create Stream
  3. Select Inbound as the stream type
  4. Give it a descriptive name (e.g., "Support Inbox")

Step 2: Configure Your Webhook

  1. Go to the stream's Setup or Settings tab
  2. Enter your webhook URL (e.g., https://yourapp.com/webhooks/inbound)
  3. Optionally add a webhook secret for signature verification
  4. Click Save

Step 3: Send a Test Email

Send an email to your stream's inbound address:

To: abc123@inbound.usetransactional.com
Subject: Test email
Body: Hello, this is a test!

Your webhook should receive the parsed email data within seconds.

Webhook Payload

When an email arrives, your webhook receives a JSON payload:

{
  "event": "inbound.received",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "id": 12345,
    "messageId": "<abc123@example.com>",
    "from": {
      "email": "sender@example.com",
      "name": "John Doe"
    },
    "to": [
      { "email": "support@yourapp.com", "name": null }
    ],
    "cc": [],
    "subject": "Question about my order",
    "textBody": "Hi, I have a question about order #12345...",
    "htmlBody": "<html><body><p>Hi, I have a question...</p></body></html>",
    "attachmentCount": 1,
    "attachments": [
      {
        "filename": "receipt.pdf",
        "contentType": "application/pdf",
        "size": 102400
      }
    ],
    "spamScore": 1.5,
    "receivedAt": "2024-01-15T10:29:55.000Z",
    "authentication": {
      "spf": "pass",
      "dkim": "pass",
      "dmarc": "pass"
    },
    "rawEmailS3Key": "raw/123/456/abc-uuid.eml"
  }
}

Webhook Headers

Each webhook request includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Webhook-Eventinbound.received
X-Webhook-TimestampISO 8601 timestamp
X-SignatureHMAC-SHA256 signature (if secret configured)
X-Signature-AlgorithmHMAC-SHA256

Signature Verification

If you configure a webhook secret, verify signatures to ensure requests are authentic:

import crypto from 'crypto';
 
function verifyWebhookSignature(
  secret: string,
  timestamp: string,
  payload: string,
  signature: string
): boolean {
  const signaturePayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signaturePayload)
    .digest('hex');
 
  return `sha256=${expectedSignature}` === signature;
}
 
// In your webhook handler
app.post('/webhooks/inbound', (req, res) => {
  const timestamp = req.headers['x-webhook-timestamp'];
  const signature = req.headers['x-signature'];
  const payload = JSON.stringify(req.body);
 
  if (!verifyWebhookSignature(WEBHOOK_SECRET, timestamp, payload, signature)) {
    return res.status(401).send('Invalid signature');
  }
 
  // Process the email...
  res.status(200).send('OK');
});

Handling Attachments

Attachments under 10MB have their content included in the webhook payload (base64 encoded). For larger attachments or raw email access, use the S3 key:

// Attachments in payload
for (const attachment of data.attachments) {
  if (attachment.content) {
    // Base64 encoded content available
    const buffer = Buffer.from(attachment.content, 'base64');
    await saveFile(attachment.filename, buffer);
  }
}
 
// Or fetch from S3 (if rawEmailS3Key provided)
if (data.rawEmailS3Key) {
  const rawEmail = await s3.getObject({
    Bucket: 'transactional-emails',
    Key: data.rawEmailS3Key
  });
}

Spam Filtering

Spam Score

Every inbound email includes a spam score (0-30+):

ScoreInterpretation
0-3Very likely legitimate
4-6Probably legitimate
7-10Suspicious
11+Likely spam

Configuring Spam Threshold

Set a threshold to automatically block high-spam emails:

  1. Go to stream Settings
  2. Under Spam Filtering, set a threshold (e.g., 10)
  3. Emails above this score are blocked and not delivered to your webhook

Authentication Results

Check the authentication object to verify sender legitimacy:

const { spf, dkim, dmarc } = data.authentication;
 
if (spf === 'fail' || dkim === 'fail') {
  // Potentially spoofed sender - handle with caution
}
 
if (dmarc === 'pass') {
  // Sender domain is authenticated
}

Inbound Rules

Filter emails before they reach your webhook:

Block Rules

Block specific senders or domains:

  • spam@example.com - Block specific email
  • example.com - Block entire domain
  • *.example.com - Block domain and all subdomains

Allow Rules

Create an allowlist (only these senders get through):

  • Add allow rules for trusted senders
  • All other emails are blocked

Note: Block rules take precedence over allow rules.

Retry Behavior

If your webhook returns an error (non-2xx status), we retry with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
415 minutes
51 hour

After 5 failed attempts, the email is marked as failed. Check your stream's Inbound tab to see delivery status.

Best Practices

1. Respond Quickly

Return a 200 response as soon as possible. Process emails asynchronously:

app.post('/webhooks/inbound', async (req, res) => {
  // Acknowledge receipt immediately
  res.status(200).send('OK');
 
  // Process asynchronously
  processEmail(req.body).catch(console.error);
});

2. Handle Duplicates

Emails may occasionally be delivered twice. Use the messageId to deduplicate:

const processed = await redis.get(`email:${data.messageId}`);
if (processed) return;
 
await redis.set(`email:${data.messageId}`, '1', 'EX', 86400);
// Process email...

3. Validate Signatures

Always verify webhook signatures in production to prevent spoofing.

4. Monitor Spam Scores

Log and monitor spam scores to tune your threshold over time.

Common Use Cases

Support Inbox

if (data.subject.toLowerCase().includes('urgent')) {
  await createHighPriorityTicket(data);
} else {
  await createTicket(data);
}

Email Parsing

// Extract order number from email
const orderMatch = data.textBody.match(/order #(\d+)/i);
if (orderMatch) {
  await linkEmailToOrder(data.id, orderMatch[1]);
}

Auto-Response

// Send acknowledgment
await transactional.emails.send({
  from: 'support@yourapp.com',
  to: data.from.email,
  subject: `Re: ${data.subject}`,
  text: 'Thanks for your email. We\'ll respond within 24 hours.',
  stream: 'transactional'
});

Next Steps