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
- Email Arrives — An email is sent to your inbound address
- Processing — Transactional processes the email:
- Parses headers, body, and attachments
- Runs spam and malware checks
- Validates authentication (SPF, DKIM, DMARC)
- 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
- Navigate to your email server in the dashboard
- Click Create Stream
- Select Inbound as the stream type
- Give it a descriptive name (e.g., "Support Inbox")
Step 2: Configure Your Webhook
- Go to the stream's Setup or Settings tab
- Enter your webhook URL (e.g.,
https://yourapp.com/webhooks/inbound) - Optionally add a webhook secret for signature verification
- 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:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Event | inbound.received |
X-Webhook-Timestamp | ISO 8601 timestamp |
X-Signature | HMAC-SHA256 signature (if secret configured) |
X-Signature-Algorithm | HMAC-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+):
| Score | Interpretation |
|---|---|
| 0-3 | Very likely legitimate |
| 4-6 | Probably legitimate |
| 7-10 | Suspicious |
| 11+ | Likely spam |
Configuring Spam Threshold
Set a threshold to automatically block high-spam emails:
- Go to stream Settings
- Under Spam Filtering, set a threshold (e.g., 10)
- 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 emailexample.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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 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
- Inbound Domain Forwarding - Use your own domain
- Webhooks - Webhook configuration details
- Suppressions - Manage email suppressions
On This Page
- Overview
- How It Works
- Setting Up Inbound Processing
- Step 1: Create an Inbound Stream
- Step 2: Configure Your Webhook
- Step 3: Send a Test Email
- Webhook Payload
- Webhook Headers
- Signature Verification
- Handling Attachments
- Spam Filtering
- Spam Score
- Configuring Spam Threshold
- Authentication Results
- Inbound Rules
- Block Rules
- Allow Rules
- Retry Behavior
- Best Practices
- 1. Respond Quickly
- 2. Handle Duplicates
- 3. Validate Signatures
- 4. Monitor Spam Scores
- Common Use Cases
- Support Inbox
- Email Parsing
- Auto-Response
- Next Steps