Pipeline notePlatform-domain onboarding now publishes DMARC in monitoring mode by default.Read the record

Inbound contract

Inbound routing and the delivery pipeline

What Mailhaven expects from your provider, what it does with a delivery, and how to read the record when something goes wrong.

Mailhaven exposes one signed inbound endpoint per account and one shared endpoint for platform-domain tenants. Both reject anything they cannot verify. This page documents the exact contract: the headers we check, the deduplication keys, the retry schedule for content fetches and the fields you can inspect from your own activity log.

Endpoints

Every account gets its own inbound endpoint keyed by account slug, verified against the signing secret you supplied. Tenants on the done-for-you path share a single operator endpoint and are routed by the received_for field on the delivery.

Bring-your-own
POST /webhook/inbound/<account-slug>
Platform domain
POST /webhook/inbound/platform
Content type
application/json
Expected event
email.received

Signature verification

The signed content is the concatenation of the delivery id, the timestamp and the raw request body, separated by full stops. The HMAC is computed with the base64-decoded portion of the whsec_ secret and compared in constant time. Deliveries outside the replay window are refused even if the signature is otherwise correct.

Headers read
svix-id · svix-timestamp · svix-signature
Fallback headers
webhook-id · webhook-timestamp · webhook-signature
Signed content
${id}.${timestamp}.${rawBody}
Algorithm
HMAC-SHA256, base64, constant-time compare
Replay window
300 seconds
Rejection
401 unverified — recorded, never processed

Deduplication

A delivery is identified by the provider message id and by the Message-ID header, each scoped to the receiving account. A repeat of either creates no second message; the delivery is still recorded so the duplicate is visible in your activity log.

Primary key
unique(accountId, resendMessageId)
Secondary key
unique(accountId, messageIdHeader)
Repeat outcome
DUPLICATE — existing message untouched
Race outcome
Unique violation caught and reported as DUPLICATE

Content fetch and retries

Message metadata is committed before the fetch job is enqueued, so a failure during collection can never lose the message. The job reads the archived payload first and falls back to the provider API when the archive holds no body.

Job type
FETCH_MESSAGE_CONTENT
Attempts
6 by default
Backoff
5s · 10s · 20s · 40s · 80s · capped at 30m, with jitter
Claiming
SELECT … FOR UPDATE SKIP LOCKED
Exhausted
DEAD — retryable by the operator, message retained

Threading

Threads are resolved from In-Reply-To first, then the References chain, both matched against Message-IDs already stored for your account. When a client sends neither, a normalised subject plus a shared participant inside a thirty-day window keeps the conversation together.

Priority
In-Reply-To → References → subject window → new thread
Outbound headers
Message-ID, In-Reply-To, References
Reference chain
Parent chain plus parent Message-ID, trimmed to 20

Spam routing

Authentication results carry the most weight, followed by content heuristics. Scored mail is filed in Spam with the reasons attached and is never deleted. The threshold is an operator setting.

Inputs
SPF · DKIM · DMARC · link density · display-name spoofing
Above threshold
Filed in Spam with visible reasons
Never
Automatic deletion

When a delivery still will not behave

Your own activity log lists every delivery with its verification result, processing outcome and error text, plus the fetch jobs behind it. If the record does not explain it, send the delivery id to the postmaster and it will be traced from the archived payload.