Webhooks
Receive real-time HTTPS notifications when events occur in your account.
Overview
Webhooks allow your application to receive real-time POST requests whenever specific events happen in your mailservr account, such as receiving an email or creating a new thread.
Webhooks are available on the Max plan. You can register up to 5 webhook endpoints from Settings.
Discord webhooks
Choose Discord embed webhook when adding an endpoint and paste a direct incoming webhook URL copied from Discord. Mailservr posts native Discord embeds and does not send or require an HMAC secret for this format.
Only official discord.com, discordapp.com,canary, and ptb webhook URLs are accepted. Discord-compatible Slack or GitHub paths are intentionally rejected.
Events
email.received
Fired when an inbound email is received by one of your aliases.
{
"event": "email.received",
"data": {
"emailId": 150,
"threadId": 87,
"from": {
"address": "[email protected]",
"name": "Jane Doe"
},
"to": ["[email protected]"],
"cc": [],
"bcc": [],
"subject": "Meeting tomorrow",
"textBody": "Hey, are we still on for tomorrow?",
"htmlBody": null,
"snippet": "Hey, are we still on for...",
"receivedAt": "2026-02-13T14:30:00.000Z"
}
}email.sent
Fired when you send an email (compose or reply).
{
"event": "email.sent",
"data": {
"emailId": 151,
"threadId": 87,
"from": "[email protected]",
"to": ["[email protected]"],
"cc": [],
"subject": "Re: Meeting tomorrow",
"textBody": "Yes, see you then.",
"htmlBody": null,
"sentAt": "2026-02-13T15:00:00.000Z"
}
}thread.created
Fired when a new email thread is created (from an inbound email or a new compose).
{
"event": "thread.created",
"data": {
"threadId": 87,
"subject": "Meeting tomorrow",
"addressId": 12,
"createdAt": "2026-02-13T14:30:00.000Z"
}
}Alias lifecycle events
In addition to mail events, you can subscribe to address.created,address.paused, and address.resumed. These use the same standard JSON envelope and become concise status embeds for Discord endpoints.
Request Format
Standard webhook deliveries are HTTPS POST requests with a JSON body and signed headers. Discord deliveries use the same HTTPS method but send a Discord embed payload without HMAC headers.
| Header | Description |
|---|---|
| Content-Type | Always application/json |
| X-Webhook-Signature | HMAC-SHA256 hex digest for verifying authenticity |
| X-Webhook-Timestamp | Unix timestamp (seconds) when the request was signed |
| X-Webhook-Delivery-Id | Stable delivery-row ID to use for idempotent processing across retries |
| User-Agent | mailservr-webhooks/1.0 |
Verifying Signatures
Standard webhook requests are signed with the secret shown when you created the endpoint. Discord embed endpoints do not use HMAC signatures because Discord authenticates the URL itself. Verify signatures on every standard request to ensure it is authentic and hasn't been tampered with.
Algorithm
- Extract the
X-Webhook-TimestampandX-Webhook-Signatureheaders - Concatenate the timestamp, a period, and the raw request body:
`${timestamp}.${body}` - Compute HMAC-SHA256 of that string using your signing secret
- Compare the hex digest to the signature header (use a timing-safe comparison)
- Optionally reject requests where the timestamp is older than 5 minutes to prevent replay attacks
Node.js
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(body, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${body}`;
const expected = createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
const sigBuf = Buffer.from(signature, "utf8");
const expBuf = Buffer.from(expected, "utf8");
if (sigBuf.length !== expBuf.length) return false;
return timingSafeEqual(sigBuf, expBuf);
}Python
import hmac
import hashlib
def verify_webhook(body: str, signature: str, timestamp: str, secret: str) -> bool:
signed_payload = f"{timestamp}.{body}"
expected = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func VerifyWebhook(body, signature, timestamp, secret string) bool {
signedPayload := fmt.Sprintf("%s.%s", timestamp, body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}Best Practices
- Always verify standard signatures. Never trust a standard webhook request without checking its HMAC signature. Discord endpoints use the private webhook URL as their credential.
- Respond quickly. Return a 2xx status code within 10 seconds. Do heavy processing asynchronously after responding.
- Use HTTPS. Webhook endpoints must use HTTPS. HTTP URLs are rejected.
- Return a final response. Redirects are never followed, and endpoints that resolve to private or reserved network addresses are rejected.
- Handle retries idempotently. A failed delivery is retried with the same
X-Webhook-Delivery-Id. Persist that value before applying side effects. - Check the timestamp. Reject requests where the
X-Webhook-Timestampis older than 5 minutes to prevent replay attacks. - Keep credentials safe. Standard signing secrets and Discord webhook URLs are credentials. If compromised, delete the endpoint and create a new one.
Delivery Behavior
- Deliveries time out after 10 seconds.
- A delivery is considered successful if your endpoint returns a 2xx status code.
- Failed deliveries are retried automatically up to five total attempts with exponential backoff starting at one minute and capped at one hour.
- Delivery logs are retained for 30 days.
- You can toggle endpoints on/off without deleting them.
Managing Webhooks
Webhook endpoints are managed from the Developers tab in Settings. You can also manage them via the API:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/webhooks | List your webhook endpoints |
| POST | /api/webhooks | Create a new endpoint |
| PATCH | /api/webhooks/:id | Update events or toggle enabled |
| DELETE | /api/webhooks/:id | Delete an endpoint |
| GET | /api/webhooks/:id/deliveries | View delivery logs |
All endpoints require authentication via session cookie or Authorization: Bearer <api-key> header.