Notifications and webhooks
mxgate tells you when mail fails: an hourly email digest and a signed webhook. The payload, the signature check in Node.js, retries and the test call.
mxgate tells you when mail does not arrive. An hourly email goes to the account address, and a signed webhook can tell your program.
There is no webhook for each received message yet. Webhooks are for mail that failed.
Events
| Event | When | Default |
|---|---|---|
bounced | The other server refused the message. | On |
expired | mxgate tried for 2 days and stopped. | On |
rejected | mxgate refused the message at SMTP time. | Off |
deferred | The other server did not accept the message yet. | Off |
The events are the statuses of the message log. The choice of events applies to the email digest and to the webhook.
The email digest
The digest is on by default. It goes to the account address at most one time each hour, and only when there is an event. It lists each message with the time, the addresses and the explanation. One digest shows at most 20 messages; the log has the others.
Change the settings
npx mxgate notify # the settings and the last 20 notifications
npx mxgate notify set --events bounced,expired,rejected
npx mxgate notify set --email off
npx mxgate notify set --webhook https://example.com/hooks/mxgate
npx mxgate notify set --webhook offThe API call is PUT /v1/notifications. Send only the fields that you change:
curl -X PUT https://api.mxgate.io/v1/notifications \
-H "Authorization: Bearer $MXGATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":true,"events":["bounced","expired","rejected"],"webhook_url":"https://example.com/hooks/mxgate"}'Set webhook_url to null to remove the webhook.
The webhook
mxgate sends one POST with a JSON body for each event. The address must be a public https:// URL with no user name or password in it. mxgate refuses private addresses, and it checks the address again when it makes the connection. mxgate does not follow redirects. An answer with a 2xx status in 10 seconds is a delivery.
{
"id": "evt_42",
"type": "message.bounced",
"created_at": "2026-11-14T10:02:12.000Z",
"data": {
"received_at": "2026-11-14T10:02:10.000Z",
"updated_at": "2026-11-14T10:02:12.000Z",
"direction": "in",
"status": "bounced",
"sender": "client@example.org",
"recipient": "sales@example.com",
"destination": "old@example.org",
"domain": "example.com",
"reason": "host mx.example.org[203.0.113.5] said: 550 5.1.1 The mailbox does not exist (in reply to RCPT TO command)",
"explanation": "The destination mailbox does not exist.",
"dsn": "5.1.1",
"relay": "mx.example.org[203.0.113.5]:25",
"client": "mail.example.org[203.0.113.9]",
"message_id": "id-of-their-mail@example.org",
"attempts": 1
}
}type is message. and the event. data is the log row, with the same fields as GET /v1/logs. It has no subject and no body.
Check the signature
Each call has this header:
mxgate-signature: t=1794650532,v1=5d2f...t is the time in Unix seconds. v1 is the HMAC-SHA256, in hex, of the text <t>.<raw body> with your webhook_secret as the key. Refuse a call when t is more than 5 minutes old. Use the raw body as it arrived, before your program parses the JSON.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the body as a string, as it arrived. header: the mxgate-signature value.
export function isFromMxgate(rawBody, header, secret) {
const t = /(?:^|,)t=(\d+)/.exec(header ?? "")?.[1];
const v1 = /(?:^|,)v1=([0-9a-f]{64})/.exec(header ?? "")?.[1];
if (!t || !v1) return false;
if (Date.now() / 1000 - Number(t) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return timingSafeEqual(expected, Buffer.from(v1, "hex"));
}The secret
A new webhook address gets a new webhook_secret. It starts with whsec_. The answer that sets the address shows the secret in full, one time: store it then. Later answers show only its first characters. If you lose the secret, remove the webhook and set it again to get a new one.
Retries
If a call fails, mxgate tries again 5 times: after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. After the last try the notification is failed. Your program can get the same event more than one time, so use id to ignore a copy.
Test the webhook
$ npx mxgate notify test
The webhook answered 200.mxgate notify test and POST /v1/notifications/test send a signed test call now and report the answer. The test call has "id": "evt_test" and "type": "test". The API answer is {ok, status, error, message}.
Recent deliveries
GET /v1/notifications gives the settings and recent: the last 20 notifications. Each one has event, created_at, recipient, webhook_status, webhook_attempts, webhook_last_error and emailed. webhook_status is none (no webhook was set), pending, delivered or failed.
The message log
The mxgate log has one row for each message: forwarded, sent, deferred, bounced, expired or rejected, with the reply of the other server.
MCP server and agents
The mxgate MCP server: a hosted server with OAuth sign-in, or a local one with DNS setup. The tools, and what an agent cannot do without you.