message.comDevelopers

Email inbound Live

Your email service provider (or our hosted SMTP) posts inbound customer emails to message.com. We parse them, dedupe on the messageId, drop auto-replies and bounce mail, and create or append to a ticket in the unified inbox.

Endpoint

POST/api/v1/email/inboundAuth: Bearer

This endpoint is HMAC-signed. The signing secret is set per workspace at Settings → Integrations → Inbound email. Configure your ESP's inbound webhook (Postmark, SendGrid Inbound Parse, Mailgun, etc.) to post here with the secret you copy from the dashboard.

Most ESPs let you set a signing secret. If your ESP does not, host a small relay that signs the payload before forwarding. The hashing details are in Signature verification.

Inbound addresses

Three flavours of inbound address feed into this endpoint:

  • Auto. We assign you [email protected]. Zero configuration.
  • Subdomain. [email protected]. Lets you map multiple local parts (e.g., billing, support) to different departments.
  • Custom domain. [email protected]. Requires MX record pointing to our inbound relay or your ESP forwarding inbound mail to us.

Manage these at Inbound addresses API.

Required headers

HeaderValue
Content-Typeapplication/json
X-Signaturesha256=<hex> over the raw request body

Request body

The shape mirrors the de-facto standard most ESPs produce. Required fields: from, to, subject, and at least one of text or html. Optional fields: messageId, headers, attachments.

Request body
{
  "from": "Customer Name <[email protected]>",
  "to": ["[email protected]"],
  "subject": "Help with my order",
  "text": "Plain text body",
  "html": "<p>HTML body</p>",
  "messageId": "<unique-id-from-esp>",
  "headers": {
    "Auto-Submitted": "...",
    "Precedence": "...",
    "In-Reply-To": "<[email protected]>",
    "References": "<[email protected]> <[email protected]>"
  },
  "attachments": [
    { "filename": "order.pdf", "url": "https://...", "size": 12345, "contentType": "application/pdf" }
  ]
}

Threading

We thread messages into existing conversations using the In-Reply-To and References headers. Messages whose references match a prior thread are appended; new threads create a new conversation. If both signals are missing, we fall back to subject-line normalisation (strip Re: / Fwd: prefixes) plus visitor email.

Deduplication

The messageId is the dedupe key. The same messageId delivered twice is a no-op. ESPs sometimes deliver duplicates after a retry; this prevents double-tickets. If your ESP does not provide a messageId, we synthesise one from (from + subject + first 200 chars of body), which is good enough for retry storms but does not catch genuine duplicates.

What we drop

The endpoint silently drops (returns 200 OK without creating a ticket) on these signals to avoid filling the inbox with noise:

  • From: MAILER-DAEMON@*, bounce messages. Handled via the outbound webhook instead.
  • Auto-Submitted: auto-replied or auto-generated, out-of-office replies.
  • Precedence: bulk or Precedence: list, mailing list traffic.
  • List-Unsubscribe header present, newsletter blasts.

Drops are logged in the workspace audit log with a reason field for forensic debugging.

Response

On success: 200 OK with an empty body. On signature failure: 401 invalid_signature. On schema failure: 400 invalid_body with details. On dedupe hit: 200 OK (idempotent).

Example request

cURL
curl -X POST 'https://app.message.com/api/v1/email/inbound' \
  -H 'Content-Type: application/json' \
  -H 'X-Signature: sha256=<hex>' \
  --data-binary @inbound.json

Verifying the signature (your side)

If you proxy or replay inbound mail through your own system, you may want to verify our outgoing signature on the relay leg. The verification logic is symmetric.

TypeScript
import { createHmac, timingSafeEqual } from "crypto";

function verifyInbound(rawBody: Buffer, signatureHeader: string, secret: string) {
  // Header format: 'sha256=<hex>'
  const [scheme, sig] = signatureHeader.split("=");
  if (scheme !== "sha256") throw new Error("bad scheme");

  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    throw new Error("bad signature");
  }
}

Full multi-language verification reference at Signature verification.

Common pitfalls

  • Parsing the body before verifying. Compute the HMAC over the raw body bytes. JSON parsers may reorder keys or normalise whitespace and break the signature.
  • Missing messageId. Without it we synthesise one. Synthesised IDs can collide on near-identical messages. Configure your ESP to forward Message-ID.
  • MX record not switched. For custom domains, mail keeps flowing to your old provider until DNS propagates. Test with a tool that resolves the MX explicitly.
  • HTML-only emails. Some senders omit text. We render the HTML via a sanitiser, but plain-text agents have a worse experience. Ask senders to include both parts when possible.