Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mailbreeze.com/llms.txt

Use this file to discover all available pages before exploring further.

When you configure a webhook destination for inbound email, MailBreeze sends a POST request to your endpoint with the full email data. This allows you to parse emails, extract information, trigger workflows, and integrate with your application.

Webhook Payload

MailBreeze sends a JSON payload with the complete email data:
{
  "id": "inb_a1b2c3d4e5f6",
  "event": "inbound.received",
  "timestamp": "2026-01-27T15:30:05.123Z",
  "data": {
    "from": {
      "address": "customer@example.com",
      "name": "Jane Customer"
    },
    "to": [
      {
        "address": "support@yourdomain.com",
        "name": null
      }
    ],
    "cc": [
      {
        "address": "manager@example.com",
        "name": "Manager"
      }
    ],
    "replyTo": {
      "address": "customer@example.com",
      "name": "Jane Customer"
    },
    "subject": "Question about my order #12345",
    "text": "Hi,\n\nI have a question about my recent order...\n\nThanks,\nJane",
    "html": "<p>Hi,</p><p>I have a question about my recent order...</p><p>Thanks,<br>Jane</p>",
    "attachments": [
      {
        "id": "att_xyz789",
        "filename": "receipt.pdf",
        "contentType": "application/pdf",
        "size": 45678,
        "url": "https://api.mailbreeze.com/v1/inbound/inb_a1b2c3d4e5f6/attachments/att_xyz789"
      }
    ],
    "headers": {
      "message-id": "<CADfH8w3abc123@mail.example.com>",
      "date": "Mon, 27 Jan 2026 10:30:00 -0500",
      "in-reply-to": "<previous-message-id@yourdomain.com>",
      "references": "<original-message-id@yourdomain.com>"
    },
    "authentication": {
      "spf": "pass",
      "dkim": "pass",
      "dmarc": "pass"
    },
    "spamScore": 0.1,
    "receivedAt": "2026-01-27T15:30:05.123Z"
  }
}

Payload Fields

id
string
Unique identifier for this inbound email.
event
string
Event type. Always inbound.received for new emails.
timestamp
string
ISO 8601 timestamp when the webhook was sent.
data.from
object
Sender information with address and name fields.
data.to
array
Array of recipient objects matching your domain.
data.cc
array
Array of CC recipient objects.
data.subject
string
Email subject line.
data.text
string
Plain text body of the email.
data.html
string
HTML body of the email (if present).
data.attachments
array
Array of attachment objects with metadata and download URLs.
data.headers
object
Key email headers including Message-ID, Date, In-Reply-To, and References.
data.authentication
object
SPF, DKIM, and DMARC check results (pass, fail, none).
data.spamScore
number
Spam score from 0.0 (not spam) to 1.0 (definitely spam).

Webhook Requirements

Your endpoint must meet these requirements:
RequirementDetails
ProtocolHTTPS only (HTTP endpoints will be rejected)
MethodAccept POST requests
ResponseReturn 200 OK within 30 seconds
Content-TypeAccept application/json
AvailabilityMust be publicly accessible
If your endpoint returns a non-2xx status code or times out, MailBreeze will retry the webhook. See Retry Behavior below.

Verifying Webhooks

To ensure webhooks are genuinely from MailBreeze, verify the signature header.

Signature Header

Every webhook includes an X-MailBreeze-Signature header:
X-MailBreeze-Signature: t=1706369405,v1=abc123def456...
The header contains:
  • t — Unix timestamp when the signature was generated
  • v1 — HMAC-SHA256 signature of the payload

Verification Steps

1

Extract the timestamp and signature

Parse the header to get t (timestamp) and v1 (signature).
const sigHeader = req.headers['x-mailbreeze-signature'];
const [tPart, v1Part] = sigHeader.split(',');
const timestamp = tPart.split('=')[1];
const signature = v1Part.split('=')[1];
2

Prepare the signed payload

Concatenate the timestamp and raw request body with a period.
const signedPayload = `${timestamp}.${rawBody}`;
3

Compute expected signature

Calculate HMAC-SHA256 using your webhook secret.
const crypto = require('crypto');
const expectedSignature = crypto
  .createHmac('sha256', webhookSecret)
  .update(signedPayload)
  .digest('hex');
4

Compare signatures

Use timing-safe comparison to prevent timing attacks.
const isValid = crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(expectedSignature)
);
5

Validate timestamp

Reject requests older than 5 minutes to prevent replay attacks.
const tolerance = 5 * 60 * 1000; // 5 minutes
const isRecent = Date.now() - (timestamp * 1000) < tolerance;

Complete Verification Example

const crypto = require('crypto');
const express = require('express');
const app = express();

// Must use raw body for signature verification
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook/inbound', (req, res) => {
  const signature = req.headers['x-mailbreeze-signature'];
  const webhookSecret = process.env.MAILBREEZE_WEBHOOK_SECRET;

  if (!verifySignature(signature, req.body, webhookSecret)) {
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(req.body);

  // Process the email
  console.log('Received email from:', payload.data.from.address);

  res.status(200).send('OK');
});

function verifySignature(sigHeader, body, secret) {
  const [tPart, v1Part] = sigHeader.split(',');
  const timestamp = tPart.split('=')[1];
  const signature = v1Part.split('=')[1];

  // Check timestamp (5 minute tolerance)
  const age = Date.now() - (parseInt(timestamp) * 1000);
  if (age > 5 * 60 * 1000) return false;

  // Compute expected signature
  const signedPayload = `${timestamp}.${body}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Retry Behavior

If your endpoint fails to respond with 200 OK, MailBreeze retries with exponential backoff:
AttemptDelay After Failure
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 hours
724 hours
After 7 failed attempts, the webhook is marked as failed. Failed webhooks are visible in Domains > Inbound > Delivery Logs.
Your endpoint should handle duplicate deliveries gracefully. Use the id field to deduplicate emails in case a webhook is retried despite successful processing.

Downloading Attachments

Attachment URLs require authentication. Include your API key when downloading:
curl -o receipt.pdf \
  -H "x-api-key: sk_live_xxx" \
  "https://api.mailbreeze.com/v1/inbound/inb_a1b2c3d4e5f6/attachments/att_xyz789"
Attachment URLs expire after 7 days. Download attachments promptly if you need to retain them.

Testing Webhooks

Local Development

Use a tunneling service to expose your local server:
ngrok
ngrok http 3000
# Use the generated URL (e.g., https://abc123.ngrok.io/webhook/inbound)

Test Endpoint

Send a test webhook from the MailBreeze dashboard:
  1. Go to Domains > your domain > Inbound Settings > Routes
  2. Select your webhook route
  3. Click Send Test Webhook
This sends a sample payload to verify your endpoint is working.

Manual Testing

Send an email to an address on your domain and monitor your endpoint logs.

Error Handling

Return appropriate HTTP status codes:
CodeMeaningMailBreeze Behavior
200SuccessMark as delivered
4xxClient error (bad request, unauthorized)Retry (may be transient)
5xxServer errorRetry with backoff
TimeoutNo response in 30sRetry with backoff
If you need more time to process an email, return 200 OK immediately and process asynchronously. Use a message queue (Redis, SQS, etc.) for heavy processing.

Common Patterns

Parse Reply-To Thread

Extract the thread ID from reply-to addresses:
// Your app sends from: reply+order_123@inbound.yourapp.com
// Customer replies to that address

app.post('/webhook/inbound', (req, res) => {
  const { to } = req.body.data;
  const replyAddress = to[0].address;

  // Extract order ID from address
  const match = replyAddress.match(/reply\+(.+)@/);
  if (match) {
    const orderId = match[1]; // "order_123"
    // Associate this reply with the order
  }

  res.status(200).send('OK');
});

Filter Spam

Use the spam score and authentication results:
app.post('/webhook/inbound', (req, res) => {
  const { spamScore, authentication } = req.body.data;

  // Reject likely spam
  if (spamScore > 0.7) {
    console.log('Rejected spam email');
    return res.status(200).send('OK'); // Still return 200 to prevent retries
  }

  // Flag suspicious emails
  if (authentication.dmarc !== 'pass') {
    console.log('Warning: DMARC check failed');
  }

  // Process legitimate email
  // ...

  res.status(200).send('OK');
});

Extract Thread Context

Use headers for conversation threading:
app.post('/webhook/inbound', (req, res) => {
  const { headers } = req.body.data;

  const inReplyTo = headers['in-reply-to'];
  const references = headers['references'];

  if (inReplyTo) {
    // This is a reply to a previous message
    const originalMessageId = inReplyTo.replace(/[<>]/g, '');
    // Look up the original conversation
  }

  res.status(200).send('OK');
});

Next Steps

Configure Routes

Set up address-specific routing rules

Inbound API

Retrieve inbound emails via API