Skip to main content

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.

HeaderDescription
Content-TypeAlways application/json
X-Webhook-SignatureHMAC-SHA256 hex digest for verifying authenticity
X-Webhook-TimestampUnix timestamp (seconds) when the request was signed
X-Webhook-Delivery-IdStable delivery-row ID to use for idempotent processing across retries
User-Agentmailservr-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

  1. Extract the X-Webhook-Timestamp and X-Webhook-Signature headers
  2. Concatenate the timestamp, a period, and the raw request body: `${timestamp}.${body}`
  3. Compute HMAC-SHA256 of that string using your signing secret
  4. Compare the hex digest to the signature header (use a timing-safe comparison)
  5. 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-Timestamp is 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:

MethodEndpointDescription
GET/api/webhooksList your webhook endpoints
POST/api/webhooksCreate a new endpoint
PATCH/api/webhooks/:idUpdate events or toggle enabled
DELETE/api/webhooks/:idDelete an endpoint
GET/api/webhooks/:id/deliveriesView delivery logs

All endpoints require authentication via session cookie or Authorization: Bearer <api-key> header.